endpoint_sec/audit.rs
1//! Expose a wrapper around [`audit_token_t`]: [`AuditToken`]
2
3use std::fmt;
4#[cfg(feature = "audit_token_from_pid")]
5use std::mem;
6
7use endpoint_sec_sys::{
8 au_asid_t, audit_token_t, audit_token_to_asid, audit_token_to_auid, audit_token_to_egid, audit_token_to_euid,
9 audit_token_to_pid, audit_token_to_pidversion, audit_token_to_rgid, audit_token_to_ruid, gid_t, pid_t, uid_t,
10};
11#[cfg(feature = "audit_token_from_pid")]
12use libc::{KERN_SUCCESS, c_int};
13#[cfg(feature = "audit_token_from_pid")]
14use mach2::kern_return::kern_return_t;
15#[cfg(feature = "audit_token_from_pid")]
16use mach2::port::mach_port_name_t;
17#[cfg(feature = "audit_token_from_pid")]
18use mach2::task_info::TASK_AUDIT_TOKEN;
19
20/// A wrapper around an [`audit_token_t`].
21#[derive(Clone, Copy)]
22#[doc(alias = "audit_token_t")]
23#[repr(transparent)]
24pub struct AuditToken(pub audit_token_t);
25
26impl fmt::LowerHex for AuditToken {
27 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
28 for v in self.0.val {
29 fmt::LowerHex::fmt(&v, f)?;
30 }
31
32 Ok(())
33 }
34}
35
36impl fmt::UpperHex for AuditToken {
37 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38 for v in self.0.val {
39 fmt::UpperHex::fmt(&v, f)?;
40 }
41
42 Ok(())
43 }
44}
45
46/// Endpoint Security wrappers and test helpers
47impl AuditToken {
48 /// Get the [`AuditToken`] for the given PID, if it exists.
49 ///
50 /// Endpoint Security does not currently provide a way to get the audit tokens of processes
51 /// already existing when first connecting a client. However, it is relatively easy to list
52 /// the PIDs of the current processes. This function therefore enables to bridge this gap.
53 ///
54 /// Errors from the underlying system calls are returned directly. Although it is almost
55 /// certain that only the catch-all `KERN_FAILURE` (5) will ever be observed in practice, this
56 /// should still be useful in order to emphasize that the unexpected case should be accounted
57 /// for instead of just discarded, for example to then log.
58 ///
59 /// ## Implementation details
60 ///
61 /// Currently this method is implemented following the method [described here][method], with
62 /// calls to `task_name_for_pid` and `task_info(_, TASK_AUDIT_TOKEN, _, _)` but the first
63 /// function is marked as *obsolete* in the header containing it in macOS's SDK.
64 ///
65 /// Other possibilities could be `task_for_pid()` or `task_inspect_for_pid()`. For now, the
66 /// current implementation is the most backwards and forwards compatible considering
67 /// `task_for_pid()` now concretely requires SIP to be disabled, which thus makes it pretty
68 /// much unusable. If you find a bug/need us to use a more recent method, please signal it.
69 ///
70 /// [method]: https://developer.apple.com/forums/thread/652363
71 #[cfg(feature = "audit_token_from_pid")]
72 pub fn from_pid(pid: pid_t) -> Result<Self, kern_return_t> {
73 Ok(Self(mach_task_audit_token(mach_task_name(pid)?)?))
74 }
75
76 /// Raw underlying audit token.
77 #[inline]
78 pub fn raw_token(&self) -> &audit_token_t {
79 &self.0
80 }
81
82 /// The audit user ID.
83 ///
84 /// **NOTE**: Used to identify Mach tasks and senders of Mach messages as subjects of the audit system.
85 #[inline(always)]
86 pub fn auid(&self) -> uid_t {
87 // Safety: The audit_token_t is owned by self.
88 unsafe { audit_token_to_auid(self.0) }
89 }
90
91 /// The effective user ID.
92 ///
93 /// **NOTE**: Used to identify Mach tasks and senders of Mach messages as subjects of the audit system.
94 #[inline(always)]
95 pub fn euid(&self) -> uid_t {
96 // Safety: The audit_token_t is owned by self.
97 unsafe { audit_token_to_euid(self.0) }
98 }
99
100 /// The effective group ID.
101 ///
102 /// **NOTE**: Used to identify Mach tasks and senders of Mach messages as subjects of the audit system.
103 #[inline(always)]
104 pub fn egid(&self) -> gid_t {
105 // Safety: The audit_token_t is owned by self.
106 unsafe { audit_token_to_egid(self.0) }
107 }
108
109 /// The real user ID.
110 ///
111 /// **NOTE**: Used to identify Mach tasks and senders of Mach messages as subjects of the audit system.
112 #[inline(always)]
113 pub fn ruid(&self) -> uid_t {
114 // Safety: The audit_token_t is owned by self.
115 unsafe { audit_token_to_ruid(self.0) }
116 }
117
118 /// The real group ID.
119 ///
120 /// **NOTE**: Used to identify Mach tasks and senders of Mach messages as subjects of the audit system.
121 #[inline(always)]
122 pub fn rgid(&self) -> gid_t {
123 // Safety: The audit_token_t is owned by self.
124 unsafe { audit_token_to_rgid(self.0) }
125 }
126
127 /// The process ID.
128 ///
129 /// **NOTE**: Used to identify Mach tasks and senders of Mach messages as subjects of the audit system.
130 #[inline(always)]
131 pub fn pid(&self) -> pid_t {
132 // Safety: The audit_token_t is owned by self.
133 unsafe { audit_token_to_pid(self.0) }
134 }
135
136 /// The audit session ID.
137 ///
138 /// **NOTE**: Used to identify Mach tasks and senders of Mach messages as subjects of the audit system.
139 #[inline(always)]
140 pub fn asid(&self) -> au_asid_t {
141 // Safety: The audit_token_t is owned by self.
142 unsafe { audit_token_to_asid(self.0) }
143 }
144
145 /// The process ID version.
146 ///
147 /// **NOTE**: Used to identify Mach tasks and senders of Mach messages as subjects of the audit system.
148 #[inline(always)]
149 pub fn pidversion(&self) -> i32 {
150 // Safety: The audit_token_t is owned by self.
151 unsafe { audit_token_to_pidversion(self.0) }
152 }
153}
154
155/// Crate-private methods
156impl AuditToken {
157 /// Create a new [`AuditToken`] from [`audit_token_t`].
158 #[inline(always)]
159 pub(crate) fn new(token: audit_token_t) -> Self {
160 AuditToken(token)
161 }
162
163 /// Allow to grab a reference out of the stored token.
164 #[inline(always)]
165 pub(crate) fn get_raw_ref(&self) -> &audit_token_t {
166 &self.0
167 }
168}
169
170#[cfg(feature = "static_assertions")]
171static_assertions::assert_impl_all!(AuditToken: Send);
172
173impl_debug_eq_hash_with_functions!(
174 AuditToken;
175 auid,
176 euid,
177 egid,
178 ruid,
179 rgid,
180 pid,
181 asid,
182 pidversion,
183);
184
185/// Safe wrapper around [`task_name_for_pid`].
186#[cfg(feature = "audit_token_from_pid")]
187fn mach_task_name(pid: pid_t) -> Result<mach_port_name_t, kern_return_t> {
188 let mut task_name = mach_port_name_t::default();
189
190 // SAFETY:
191 // * `mach_task_self` is always safe to call: resolves a static variable;
192 // * `task_name` is mutable and of the correct type so the reference is
193 // aligned and points to initialized memory;
194 // * errors are checked for below;
195 let res = unsafe { task_name_for_pid(mach2::traps::mach_task_self(), pid, &mut task_name) };
196
197 if res == KERN_SUCCESS { Ok(task_name) } else { Err(res) }
198}
199
200/// Safe wrapper around [`libc::task_info`] specialized for [`TASK_AUDIT_TOKEN`].
201#[cfg(feature = "audit_token_from_pid")]
202fn mach_task_audit_token(task_name: mach_port_name_t) -> Result<audit_token_t, kern_return_t> {
203 let mut audit_token = audit_token_t::default();
204 let mut audit_token_size = mem::size_of_val(&audit_token.val) as u32;
205
206 // SAFETY:
207 // * `task_name` is initialized;
208 // * `audit_token` is mutable and of the correct type so the reference
209 // is aligned and points to initialized memory, its type is in sync
210 // with `TASK_AUDIT_TOKEN` and `audit_token_size` is its size in bytes;
211 // * errors are checked for below;
212 let res = unsafe {
213 libc::task_info(
214 task_name,
215 TASK_AUDIT_TOKEN,
216 audit_token.val.as_mut_ptr().cast(),
217 &mut audit_token_size,
218 )
219 };
220
221 if res == KERN_SUCCESS { Ok(audit_token) } else { Err(res) }
222}
223
224#[cfg(feature = "audit_token_from_pid")]
225unsafe extern "C" {
226 // TODO: Replace with the one from `mach2::traps` when
227 // https://github.com/JohnTitor/mach2/pull/71 is merged and released.
228 fn task_name_for_pid(target_tport: mach_port_name_t, pid: c_int, tn: *mut mach_port_name_t) -> kern_return_t;
229}
230
231#[cfg(test)]
232#[cfg(feature = "audit_token_from_pid")]
233mod test {
234 use std::ffi::{c_char, c_void};
235 use std::mem::MaybeUninit;
236 use std::time::Duration;
237 use std::{io, ptr, thread};
238
239 use libc::{ESRCH, MAXCOMLEN};
240
241 use super::*;
242
243 #[test]
244 fn audit_token_from_pid() {
245 for proc in get_proc_infos().unwrap() {
246 let pid = proc.pbsi_pid;
247 let proc_euid = proc.pbsi_uid;
248 let proc_egid = proc.pbsi_gid;
249
250 let audit_token = match AuditToken::from_pid(pid as pid_t) {
251 Ok(at) => at,
252 Err(err) => {
253 // The error is not filterable and can simply be due to the
254 // process having exited since, so check for that before panicking.
255 // Wait a bit before doing so to avoid flakiness: it seems to
256 // work, so could be due to a small race in case the process
257 // is currently exiting or whatever.
258 thread::sleep(Duration::from_millis(100));
259
260 if proc_is_alive(pid as pid_t).unwrap() {
261 panic!(
262 "`AuditToken::from_pid({})` failed while the process is still alive: {:?}",
263 pid, err,
264 )
265 } else {
266 continue;
267 }
268 },
269 };
270
271 assert_eq!(proc_euid, audit_token.euid());
272 assert_eq!(proc_egid, audit_token.egid());
273 assert_eq!(pid, audit_token.pid() as u32);
274 }
275 }
276
277 /// A convenience composition of [`get_procs`] and [`get_proc_info`].
278 fn get_proc_infos() -> Result<Vec<proc_bsdshortinfo>, io::Error> {
279 get_procs()?.into_iter().map(get_proc_info).collect()
280 }
281
282 /// Returns the list of the PIDs of the processes currently running.
283 fn get_procs() -> Result<Vec<pid_t>, io::Error> {
284 // SAFETY: giving a null pointer and a zero size returns the process count.
285 let count = unsafe { libc::proc_listallpids(ptr::null_mut(), 0) };
286
287 if count <= 0 {
288 return Err(io::Error::last_os_error());
289 }
290
291 let mut pids = Vec::<pid_t>::with_capacity(count as usize);
292 let ffi_pids = pids.spare_capacity_mut();
293 // SAFETY:
294 // * the given buffer is correctly allocated for `count` elements;
295 // * the specified buffer size is consistent with it and its elements;
296 let new_count = unsafe {
297 libc::proc_listallpids(
298 ffi_pids.as_mut_ptr().cast::<c_void>(),
299 (ffi_pids.len() * mem::size_of::<pid_t>()) as i32,
300 )
301 };
302
303 if new_count <= 0 || new_count > count {
304 Err(io::Error::last_os_error())
305 } else {
306 // Set the vector length to what was actually written.
307 // SAFETY: the `new_count` can only be <= to the capacity here.
308 unsafe { pids.set_len(new_count as usize) };
309 Ok(pids)
310 }
311 }
312
313 /// Returns the short BSD info of the process identified by the given PID.
314 fn get_proc_info(pid: pid_t) -> Result<proc_bsdshortinfo, io::Error> {
315 let mut info = MaybeUninit::<proc_bsdshortinfo>::uninit();
316 // SAFETY:
317 // * the buffer pointer points to an available stack space;
318 // * the specified buffer size is consistent with it and its elements;
319 // * the specified info type is consistent with the buffer;
320 let res = unsafe {
321 libc::proc_pidinfo(
322 pid,
323 PROC_PIDT_SHORTBSDINFO,
324 0,
325 ptr::from_mut(&mut info).cast(),
326 mem::size_of::<proc_bsdshortinfo>() as _,
327 )
328 };
329
330 if res != mem::size_of::<proc_bsdshortinfo>() as c_int {
331 Err(io::Error::last_os_error())
332 } else {
333 // SAFETY: the call succeeded at this point.
334 Ok(unsafe { info.assume_init() })
335 }
336 }
337
338 /// Returns `true` if the process identified by the given PID is still alive.
339 fn proc_is_alive(pid: pid_t) -> Result<bool, io::Error> {
340 // A signal of 0 only performs a liveness check.
341 // SAFETY: always safe to call.
342 if unsafe { libc::kill(pid, 0) } == 0 {
343 return Ok(true);
344 }
345
346 // `kill` failed but it might not be because the process is dead.
347 let err = io::Error::last_os_error();
348
349 // If `errno` is equal to `ESCHR`, then it means the process is dead.
350 // UNWRAP: the error was built from `io::Error::last_os_error`.
351 if err.raw_os_error().unwrap() == ESRCH {
352 Ok(false)
353 } else {
354 Err(err)
355 }
356 }
357
358 // TODO: remove when https://github.com/rust-lang/libc/pull/5110 is merged and released.
359
360 const PROC_PIDT_SHORTBSDINFO: c_int = 13;
361
362 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
363 #[repr(C)]
364 struct proc_bsdshortinfo {
365 /// Process ID.
366 pbsi_pid: u32,
367 /// Process parent ID.
368 pbsi_ppid: u32,
369 /// Process perp ID.
370 pbsi_pgid: u32,
371 /// `p_stat` value: `SZOMB`, `SRUN`, etc.
372 pbsi_status: u32,
373 /// Up to 16 characters of process name.
374 pbsi_comm: [c_char; MAXCOMLEN],
375 /// 64bit, emulated, etc.
376 pbsi_flags: u32,
377 /// Current UID on process.
378 pbsi_uid: uid_t,
379 /// Current GID on process.
380 pbsi_gid: gid_t,
381 /// Current RUID on process.
382 pbsi_ruid: uid_t,
383 /// Current RGID on process.
384 pbsi_rgid: gid_t,
385 /// Current SVUID on process.
386 pbsi_svuid: uid_t,
387 /// Current SVGID on process.
388 pbsi_svgid: gid_t,
389 /// Reserved for future use.
390 pbsi_rfu: u32,
391 }
392}