Skip to main content

rightkit_child_process/foreign/
scan.rs

1//! Finding, and optionally stopping, processes by executable path and command line.
2//!
3//! [`ProcessQuery`] describes which processes are wanted: the exact executable path, exact
4//! argument tokens, substrings of single arguments, or any combination (all criteria must hold).
5//! [`find_processes`] lists the matches without touching them; [`sweep_matching`] lists and then
6//! stops them through [`sweep`](super::sweep), so every signal is gated by the pid-plus-start-time
7//! identity of [`AdoptedIdentity`].
8//!
9//! # Safety rules
10//!
11//! * A query with no criterion is refused (`InvalidInput`), as is an empty path, token or
12//!   substring: nothing may match "everything".
13//! * Only processes owned by the current user are considered (Unix: real and effective uid;
14//!   Windows: the process token's user SID). A process whose owner cannot be read is skipped.
15//! * This process and every ancestor of it are never matched.
16//! * A candidate is described twice, before and after its command line is read, and is kept only
17//!   if both reads agree on the start time. The identity it carries was therefore read from one
18//!   process, and a later signal is refused if the pid has changed hands since.
19//! * A process whose executable or command line cannot be read does not match (an unreadable
20//!   field is never treated as a wildcard).
21//! * [`MatchSweepOptions::dry_run`] returns the matches without sending anything.
22//!
23//! # Limits
24//!
25//! * The command line is what the operating system reports. A program that rewrites its own
26//!   argument area (Linux and macOS `setproctitle` style) is matched on the rewritten text.
27//! * The executable path is compared as the OS reports it (Linux: the `/proc/<pid>/exe` target, so
28//!   a deleted or replaced binary reads as `... (deleted)` and does not match; macOS:
29//!   `proc_pidpath`; Windows: `QueryFullProcessImageNameW`, compared case-insensitively). On Unix
30//!   the wanted path is matched both as given and as canonicalized.
31//! * A process that `exec`s a different program between the match and the signal keeps its pid and
32//!   start time, so the identity check cannot see it. The window is the time between the scan and
33//!   the sweep.
34//! * Unix: the identity check and `kill` are two system calls (see the module documentation of
35//!   [`foreign`](super)). Windows closes that gap with one handle.
36//! * Supported on Linux, macOS and Windows. Other hosts return `ErrorKind::Unsupported`.
37
38use std::{
39    collections::{HashMap, HashSet},
40    ffi::OsString,
41    io,
42    num::NonZeroU32,
43    path::{Path, PathBuf},
44    time::Duration,
45};
46
47use super::{sweep, sys, AdoptedIdentity, SweepReport};
48
49/// Which processes to find. All criteria that are set must hold. Built with the methods below;
50/// at least one criterion is required.
51#[derive(Clone, Debug, Default, Eq, PartialEq)]
52pub struct ProcessQuery {
53    exe: Option<PathBuf>,
54    args_exact: Vec<OsString>,
55    args_containing: Vec<String>,
56}
57
58impl ProcessQuery {
59    pub fn new() -> Self {
60        Self::default()
61    }
62
63    /// The process runs exactly this executable (compared case-insensitively on Windows).
64    pub fn exe(mut self, path: impl Into<PathBuf>) -> Self {
65        self.exe = Some(path.into());
66        self
67    }
68
69    /// Some element of the command line (the program name included) equals `token` exactly.
70    /// May be given more than once: each token must be present.
71    pub fn arg(mut self, token: impl Into<OsString>) -> Self {
72        self.args_exact.push(token.into());
73        self
74    }
75
76    /// Some single element of the command line contains `needle` (case-sensitive; the program
77    /// name included). May be given more than once: each needle must be found, in any element.
78    pub fn arg_containing(mut self, needle: impl Into<String>) -> Self {
79        self.args_containing.push(needle.into());
80        self
81    }
82
83    fn validate(&self) -> io::Result<()> {
84        let invalid = |message: &'static str| io::Error::new(io::ErrorKind::InvalidInput, message);
85        if self.exe.is_none() && self.args_exact.is_empty() && self.args_containing.is_empty() {
86            return Err(invalid("a process query needs at least one criterion"));
87        }
88        if self
89            .exe
90            .as_ref()
91            .is_some_and(|path| path.as_os_str().is_empty())
92        {
93            return Err(invalid("the executable path of a process query is empty"));
94        }
95        if self.args_exact.iter().any(|token| token.is_empty()) {
96            return Err(invalid("an argument token of a process query is empty"));
97        }
98        if self.args_containing.iter().any(String::is_empty) {
99            return Err(invalid("an argument substring of a process query is empty"));
100        }
101        Ok(())
102    }
103
104    fn needs_args(&self) -> bool {
105        !self.args_exact.is_empty() || !self.args_containing.is_empty()
106    }
107}
108
109/// A process that satisfied a [`ProcessQuery`].
110#[derive(Clone, Debug, Eq, PartialEq)]
111pub struct MatchedProcess {
112    identity: AdoptedIdentity,
113    args: Option<Vec<OsString>>,
114}
115
116impl MatchedProcess {
117    pub fn pid(&self) -> u32 {
118        self.identity.pid()
119    }
120
121    /// The pid with the start time it had when matched: pass it to [`sweep`](super::sweep) or
122    /// record it.
123    pub fn identity(&self) -> &AdoptedIdentity {
124        &self.identity
125    }
126
127    /// The executable path as the OS reported it, when it could be read.
128    pub fn exe(&self) -> Option<&Path> {
129        self.identity.exe()
130    }
131
132    /// The command line elements (program name first), present only when the query had an
133    /// argument criterion.
134    pub fn args(&self) -> Option<&[OsString]> {
135        self.args.as_deref()
136    }
137}
138
139/// How [`sweep_matching`] behaves.
140#[derive(Clone, Debug, Eq, PartialEq)]
141pub struct MatchSweepOptions {
142    grace: Duration,
143    dry_run: bool,
144}
145
146impl Default for MatchSweepOptions {
147    fn default() -> Self {
148        Self {
149            grace: Duration::from_secs(2),
150            dry_run: false,
151        }
152    }
153}
154
155impl MatchSweepOptions {
156    pub fn new() -> Self {
157        Self::default()
158    }
159
160    /// How long matches get to leave after the graceful signal before the forced one (Unix; on
161    /// Windows there is one way to stop a process). Default 2 s.
162    pub fn grace(mut self, grace: Duration) -> Self {
163        self.grace = grace;
164        self
165    }
166
167    /// Return the matches and send nothing.
168    pub fn dry_run(mut self, dry_run: bool) -> Self {
169        self.dry_run = dry_run;
170        self
171    }
172}
173
174/// The result of [`sweep_matching`].
175#[derive(Clone, Debug, Eq, PartialEq)]
176pub struct MatchSweepReport {
177    /// Everything the query matched, newest process first.
178    pub matched: Vec<MatchedProcess>,
179    /// Whether this was a dry run (then `sweep` is empty and nothing was sent).
180    pub dry_run: bool,
181    /// What happened to the matches, as pids. Empty for a dry run.
182    pub sweep: SweepReport,
183}
184
185/// Every process that satisfies `query`, newest first (children tend to come before the
186/// parents that started them). Nothing is signalled. Processes that cannot be inspected
187/// (they exited, or belong to another user, or deny access) are skipped, never an error.
188pub fn find_processes(query: &ProcessQuery) -> io::Result<Vec<MatchedProcess>> {
189    query.validate()?;
190    let wanted_exe = query.exe.as_deref().map(exe_candidates);
191    let me = os::Me::current()?;
192    let pairs = sys::process_pairs()?;
193    let this = std::process::id();
194    let excluded = ancestors(&pairs, this);
195    let mut found = Vec::new();
196    for &(pid, _) in &pairs {
197        if pid == 0 || pid == this || excluded.contains(&pid) {
198            continue;
199        }
200        if let Ok(Some(matched)) = inspect(query, wanted_exe.as_deref(), &me, pid) {
201            found.push(matched);
202        }
203    }
204    found.sort_by_key(|matched| std::cmp::Reverse(matched.identity.start_token()));
205    Ok(found)
206}
207
208/// [`find_processes`], then stop what was found with the identity-checked [`sweep`] (unless
209/// `options` ask for a dry run).
210pub fn sweep_matching(
211    query: &ProcessQuery,
212    options: &MatchSweepOptions,
213) -> io::Result<MatchSweepReport> {
214    let matched = find_processes(query)?;
215    if options.dry_run || matched.is_empty() {
216        return Ok(MatchSweepReport {
217            matched,
218            dry_run: options.dry_run,
219            sweep: SweepReport::default(),
220        });
221    }
222    let targets: Vec<AdoptedIdentity> =
223        matched.iter().map(|found| found.identity.clone()).collect();
224    let report = sweep(&targets, options.grace)?;
225    Ok(MatchSweepReport {
226        matched,
227        dry_run: false,
228        sweep: report,
229    })
230}
231
232/// The ancestors of `this` according to the (pid, parent) table, with a guard against a table
233/// that loops because a parent pid was recycled.
234fn ancestors(pairs: &[(u32, u32)], this: u32) -> HashSet<u32> {
235    let parents: HashMap<u32, u32> = pairs.iter().copied().collect();
236    let mut found = HashSet::new();
237    let mut current = this;
238    while let Some(&parent) = parents.get(&current) {
239        if parent == 0 || parent == this || !found.insert(parent) {
240            break;
241        }
242        current = parent;
243    }
244    found
245}
246
247fn inspect(
248    query: &ProcessQuery,
249    wanted_exe: Option<&[PathBuf]>,
250    me: &os::Me,
251    pid: u32,
252) -> io::Result<Option<MatchedProcess>> {
253    let Some(nonzero) = NonZeroU32::new(pid) else {
254        return Ok(None);
255    };
256    let Some(first) = sys::read(pid)? else {
257        return Ok(None);
258    };
259    // Cheap rejection first: the executable is already in hand.
260    if let Some(wanted) = wanted_exe {
261        if !first
262            .exe
263            .as_deref()
264            .is_some_and(|seen| exe_matches(seen, wanted))
265        {
266            return Ok(None);
267        }
268    }
269    if !me.owns(pid)? {
270        return Ok(None);
271    }
272    let args = if query.needs_args() {
273        let Some(args) = os::command_line(pid)? else {
274            return Ok(None);
275        };
276        if !args_match(query, &args) {
277            return Ok(None);
278        }
279        Some(args)
280    } else {
281        None
282    };
283    // The process must still be the one first described, so the executable, the owner and the
284    // command line all belong to one process; the executable is judged on this last read.
285    let Some(last) = sys::read(pid)? else {
286        return Ok(None);
287    };
288    if last.start != first.start {
289        return Ok(None);
290    }
291    if let Some(wanted) = wanted_exe {
292        if !last
293            .exe
294            .as_deref()
295            .is_some_and(|seen| exe_matches(seen, wanted))
296        {
297            return Ok(None);
298        }
299    }
300    Ok(Some(MatchedProcess {
301        identity: AdoptedIdentity {
302            pid: nonzero,
303            start: last.start,
304            exe: last.exe,
305        },
306        args,
307    }))
308}
309
310fn args_match(query: &ProcessQuery, args: &[OsString]) -> bool {
311    query
312        .args_exact
313        .iter()
314        .all(|token| args.iter().any(|arg| arg == token))
315        && query.args_containing.iter().all(|needle| {
316            args.iter()
317                .any(|arg| arg.to_string_lossy().contains(needle.as_str()))
318        })
319}
320
321/// The wanted path as given, and on Unix also resolved (the OS reports resolved paths).
322#[cfg(unix)]
323fn exe_candidates(wanted: &Path) -> Vec<PathBuf> {
324    let mut candidates = vec![wanted.to_path_buf()];
325    if let Ok(resolved) = std::fs::canonicalize(wanted) {
326        if resolved != wanted {
327            candidates.push(resolved);
328        }
329    }
330    candidates
331}
332
333#[cfg(not(unix))]
334fn exe_candidates(wanted: &Path) -> Vec<PathBuf> {
335    vec![wanted.to_path_buf()]
336}
337
338fn exe_matches(seen: &Path, wanted: &[PathBuf]) -> bool {
339    wanted.iter().any(|path| same_path(seen, path))
340}
341
342#[cfg(windows)]
343fn same_path(a: &Path, b: &Path) -> bool {
344    fn normal(path: &Path) -> String {
345        let text = path.to_string_lossy().to_lowercase().replace('/', "\\");
346        text.strip_prefix("\\\\?\\").unwrap_or(&text).to_owned()
347    }
348    normal(a) == normal(b)
349}
350
351#[cfg(not(windows))]
352fn same_path(a: &Path, b: &Path) -> bool {
353    a == b
354}
355
356/// Splits a Windows command line the way the C runtime does (backslash runs before a quote,
357/// doubled quotes inside quotes, the program name without escapes).
358#[cfg(any(windows, test))]
359fn split_windows_command_line(line: &str) -> Vec<String> {
360    let chars: Vec<char> = line.chars().collect();
361    let n = chars.len();
362    let blank = |c: char| c == ' ' || c == '\t';
363    let mut i = 0;
364    let mut out = Vec::new();
365    while i < n && blank(chars[i]) {
366        i += 1;
367    }
368    if i >= n {
369        return out;
370    }
371    let mut program = String::new();
372    if chars[i] == '"' {
373        i += 1;
374        while i < n && chars[i] != '"' {
375            program.push(chars[i]);
376            i += 1;
377        }
378        if i < n {
379            i += 1;
380        }
381    } else {
382        while i < n && !blank(chars[i]) {
383            program.push(chars[i]);
384            i += 1;
385        }
386    }
387    out.push(program);
388    loop {
389        while i < n && blank(chars[i]) {
390            i += 1;
391        }
392        if i >= n {
393            break;
394        }
395        let mut arg = String::new();
396        let mut quoted = false;
397        while i < n {
398            let c = chars[i];
399            if c == '\\' {
400                let mut run = 0;
401                while i < n && chars[i] == '\\' {
402                    run += 1;
403                    i += 1;
404                }
405                if i < n && chars[i] == '"' {
406                    arg.extend(std::iter::repeat_n('\\', run / 2));
407                    if run % 2 == 1 {
408                        arg.push('"');
409                        i += 1;
410                    }
411                } else {
412                    arg.extend(std::iter::repeat_n('\\', run));
413                }
414            } else if c == '"' {
415                if quoted && i + 1 < n && chars[i + 1] == '"' {
416                    arg.push('"');
417                    i += 2;
418                } else {
419                    quoted = !quoted;
420                    i += 1;
421                }
422            } else if blank(c) && !quoted {
423                break;
424            } else {
425                arg.push(c);
426                i += 1;
427            }
428        }
429        out.push(arg);
430    }
431    out
432}
433
434/// The layout of the macOS `KERN_PROCARGS2` buffer: an `int` argc, the executable path, padding
435/// NULs, then argc NUL-terminated arguments, then the environment. `None` when truncated.
436#[cfg(any(target_os = "macos", all(test, unix)))]
437fn parse_procargs2(buffer: &[u8]) -> Option<Vec<OsString>> {
438    use std::os::unix::ffi::OsStringExt;
439    let count = i32::from_ne_bytes(buffer.get(..4)?.try_into().ok()?);
440    let count = usize::try_from(count).ok()?;
441    let mut i = 4;
442    while i < buffer.len() && buffer[i] != 0 {
443        i += 1;
444    }
445    while i < buffer.len() && buffer[i] == 0 {
446        i += 1;
447    }
448    let mut args = Vec::with_capacity(count);
449    for _ in 0..count {
450        if i >= buffer.len() {
451            return None;
452        }
453        let start = i;
454        while i < buffer.len() && buffer[i] != 0 {
455            i += 1;
456        }
457        if i >= buffer.len() {
458            return None; // the last argument has no terminator: the buffer was cut short
459        }
460        args.push(OsString::from_vec(buffer[start..i].to_vec()));
461        i += 1;
462    }
463    Some(args)
464}
465
466#[cfg(any(target_os = "linux", target_os = "android"))]
467mod os {
468    use std::{ffi::OsString, io, os::unix::ffi::OsStringExt};
469
470    pub(super) struct Me {
471        uid: u32,
472        euid: u32,
473    }
474
475    impl Me {
476        pub(super) fn current() -> io::Result<Self> {
477            // SAFETY: getuid and geteuid take no arguments and cannot fail.
478            Ok(Self {
479                uid: unsafe { libc::getuid() },
480                euid: unsafe { libc::geteuid() },
481            })
482        }
483
484        /// Whether `pid` has this user as real and effective uid. Not readable: `false`.
485        pub(super) fn owns(&self, pid: u32) -> io::Result<bool> {
486            let text = match std::fs::read_to_string(format!("/proc/{pid}/status")) {
487                Ok(text) => text,
488                Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(false),
489                Err(error) => return Err(error),
490            };
491            let Some(line) = text.lines().find_map(|line| line.strip_prefix("Uid:")) else {
492                return Ok(false);
493            };
494            let mut ids = line.split_whitespace().map(|id| id.parse::<u32>().ok());
495            Ok(ids.next().flatten() == Some(self.uid) && ids.next().flatten() == Some(self.euid))
496        }
497    }
498
499    pub(super) fn command_line(pid: u32) -> io::Result<Option<Vec<OsString>>> {
500        let mut bytes = match std::fs::read(format!("/proc/{pid}/cmdline")) {
501            Ok(bytes) => bytes,
502            Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(None),
503            Err(error) => return Err(error),
504        };
505        if bytes.is_empty() {
506            return Ok(None); // a zombie or a kernel thread has no command line
507        }
508        if bytes.last() == Some(&0) {
509            bytes.pop();
510        }
511        Ok(Some(
512            bytes
513                .split(|byte| *byte == 0)
514                .map(|part| OsString::from_vec(part.to_vec()))
515                .collect(),
516        ))
517    }
518}
519
520#[cfg(target_os = "macos")]
521mod os {
522    use std::{ffi::OsString, io};
523
524    pub(super) struct Me {
525        uid: libc::uid_t,
526        euid: libc::uid_t,
527    }
528
529    impl Me {
530        pub(super) fn current() -> io::Result<Self> {
531            // SAFETY: getuid and geteuid take no arguments and cannot fail.
532            Ok(Self {
533                uid: unsafe { libc::getuid() },
534                euid: unsafe { libc::geteuid() },
535            })
536        }
537
538        /// Whether `pid` has this user as real and effective uid. Not readable: `false`.
539        pub(super) fn owns(&self, pid: u32) -> io::Result<bool> {
540            // SAFETY: an all-zero `proc_bsdinfo` is a valid value (integers and char arrays).
541            let mut info: libc::proc_bsdinfo = unsafe { std::mem::zeroed() };
542            let size = std::mem::size_of::<libc::proc_bsdinfo>() as libc::c_int;
543            // SAFETY: `info` is a writable buffer of exactly `size` bytes.
544            let written = unsafe {
545                libc::proc_pidinfo(
546                    pid as libc::pid_t,
547                    libc::PROC_PIDTBSDINFO,
548                    0,
549                    (&mut info as *mut libc::proc_bsdinfo).cast(),
550                    size,
551                )
552            };
553            if written != size || info.pbi_pid != pid {
554                return Ok(false);
555            }
556            Ok(info.pbi_uid == self.euid && info.pbi_ruid == self.uid)
557        }
558    }
559
560    pub(super) fn command_line(pid: u32) -> io::Result<Option<Vec<OsString>>> {
561        let mut name = [libc::CTL_KERN, libc::KERN_PROCARGS2, pid as libc::c_int];
562        let mut capacity: usize = 0;
563        // SAFETY: a null buffer asks only for the size needed; `name` has three entries.
564        let probed = unsafe {
565            libc::sysctl(
566                name.as_mut_ptr(),
567                3,
568                std::ptr::null_mut(),
569                &mut capacity,
570                std::ptr::null_mut(),
571                0,
572            )
573        };
574        if probed != 0 {
575            return gone_or_error();
576        }
577        // Room for an argument area that grows between the two calls.
578        let mut buffer = vec![0u8; capacity + 4096];
579        let mut length = buffer.len();
580        // SAFETY: the buffer is writable for `length` bytes and `length` says so.
581        let read = unsafe {
582            libc::sysctl(
583                name.as_mut_ptr(),
584                3,
585                buffer.as_mut_ptr().cast(),
586                &mut length,
587                std::ptr::null_mut(),
588                0,
589            )
590        };
591        if read != 0 {
592            return gone_or_error();
593        }
594        buffer.truncate(length);
595        Ok(super::parse_procargs2(&buffer))
596    }
597
598    /// A process that exited, a zombie (`EINVAL`) or one this user may not read (`EPERM`)
599    /// has no command line to match; anything else is reported.
600    fn gone_or_error() -> io::Result<Option<Vec<OsString>>> {
601        let error = io::Error::last_os_error();
602        match error.raw_os_error() {
603            Some(libc::ESRCH) | Some(libc::EINVAL) | Some(libc::EPERM) => Ok(None),
604            _ => Err(error),
605        }
606    }
607}
608
609#[cfg(windows)]
610mod os {
611    use std::{ffi::c_void, ffi::OsString, io};
612
613    use windows::Win32::{
614        Foundation::{CloseHandle, ERROR_ACCESS_DENIED, ERROR_INVALID_PARAMETER, HANDLE},
615        Security::{GetTokenInformation, TokenUser, TOKEN_QUERY, TOKEN_USER},
616        System::Threading::{
617            GetCurrentProcess, OpenProcess, OpenProcessToken, PROCESS_QUERY_LIMITED_INFORMATION,
618        },
619    };
620
621    use crate::process_windows::os_error;
622
623    #[link(name = "ntdll")]
624    extern "system" {
625        fn NtQueryInformationProcess(
626            process: *mut c_void,
627            class: u32,
628            information: *mut c_void,
629            length: u32,
630            returned: *mut u32,
631        ) -> i32;
632    }
633
634    /// `ProcessCommandLineInformation`: a `UNICODE_STRING` followed by its text, Windows 8.1+.
635    const PROCESS_COMMAND_LINE_INFORMATION: u32 = 60;
636    const STATUS_INFO_LENGTH_MISMATCH: u32 = 0xC000_0004;
637    const STATUS_BUFFER_OVERFLOW: u32 = 0x8000_0005;
638    const STATUS_BUFFER_TOO_SMALL: u32 = 0xC000_0023;
639
640    #[repr(C)]
641    struct UnicodeString {
642        length: u16,
643        maximum_length: u16,
644        buffer: *const u16,
645    }
646
647    struct Owned(HANDLE);
648    impl Drop for Owned {
649        fn drop(&mut self) {
650            // SAFETY: the handle came from a successful open and is closed once.
651            let _ = unsafe { CloseHandle(self.0) };
652        }
653    }
654
655    /// `None` when the process is gone or may not be opened.
656    fn open(pid: u32) -> io::Result<Option<Owned>> {
657        // SAFETY: OpenProcess takes plain values and returns a new handle.
658        match unsafe { OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, pid) } {
659            Ok(handle) => Ok(Some(Owned(handle))),
660            Err(error)
661                if error.code() == ERROR_INVALID_PARAMETER.to_hresult()
662                    || error.code() == ERROR_ACCESS_DENIED.to_hresult() =>
663            {
664                Ok(None)
665            }
666            Err(error) => Err(os_error(error)),
667        }
668    }
669
670    /// The bytes of the user SID of `process`'s token. `None` when the token is not readable
671    /// (another user's or an elevated process).
672    fn user_sid(process: HANDLE) -> io::Result<Option<Vec<u8>>> {
673        let mut token = HANDLE::default();
674        // SAFETY: `process` is a valid handle with query access; `token` is a valid output.
675        match unsafe { OpenProcessToken(process, TOKEN_QUERY, &mut token) } {
676            Ok(()) => {}
677            Err(error) if error.code() == ERROR_ACCESS_DENIED.to_hresult() => return Ok(None),
678            Err(error) => return Err(os_error(error)),
679        }
680        let token = Owned(token);
681        let mut needed = 0u32;
682        // SAFETY: a null buffer of length 0 asks only for the size; the call is expected to fail.
683        let _ = unsafe { GetTokenInformation(token.0, TokenUser, None, 0, &mut needed) };
684        if needed == 0 {
685            return Err(io::Error::other("the token user size could not be read"));
686        }
687        // u64 elements keep the buffer aligned for TOKEN_USER.
688        let mut buffer = vec![0u64; (needed as usize).div_ceil(8)];
689        // SAFETY: the buffer is writable for `needed` bytes.
690        unsafe {
691            GetTokenInformation(
692                token.0,
693                TokenUser,
694                Some(buffer.as_mut_ptr().cast::<c_void>()),
695                needed,
696                &mut needed,
697            )
698        }
699        .map_err(os_error)?;
700        // SAFETY: the buffer now holds a TOKEN_USER; its SID pointer is checked to lie inside
701        // the buffer before it is read.
702        let user = unsafe { &*(buffer.as_ptr().cast::<TOKEN_USER>()) };
703        let sid = user.User.Sid.0 as *const u8;
704        let base = buffer.as_ptr() as usize;
705        let end = base + buffer.len() * 8;
706        if sid.is_null() || (sid as usize) < base || (sid as usize) + 8 > end {
707            return Err(io::Error::from(io::ErrorKind::InvalidData));
708        }
709        // SAFETY: the SID header (8 bytes) lies inside the buffer; byte 1 is its sub-authority count.
710        let length = 8 + 4 * usize::from(unsafe { *sid.add(1) });
711        if (sid as usize) + length > end {
712            return Err(io::Error::from(io::ErrorKind::InvalidData));
713        }
714        // SAFETY: the whole SID lies inside the buffer.
715        Ok(Some(
716            unsafe { std::slice::from_raw_parts(sid, length) }.to_vec(),
717        ))
718    }
719
720    pub(super) struct Me {
721        sid: Vec<u8>,
722    }
723
724    impl Me {
725        pub(super) fn current() -> io::Result<Self> {
726            // SAFETY: GetCurrentProcess returns a pseudo handle that needs no closing.
727            let sid = user_sid(unsafe { GetCurrentProcess() })?
728                .ok_or_else(|| io::Error::other("the current user could not be read"))?;
729            Ok(Self { sid })
730        }
731
732        /// Whether `pid`'s token names this user. Not readable: `false`.
733        pub(super) fn owns(&self, pid: u32) -> io::Result<bool> {
734            let Some(process) = open(pid)? else {
735                return Ok(false);
736            };
737            Ok(user_sid(process.0)?.is_some_and(|sid| sid == self.sid))
738        }
739    }
740
741    pub(super) fn command_line(pid: u32) -> io::Result<Option<Vec<OsString>>> {
742        let Some(process) = open(pid)? else {
743            return Ok(None);
744        };
745        let mut size: usize = 1024;
746        for _ in 0..4 {
747            let mut buffer = vec![0u64; size.div_ceil(8)];
748            let mut returned = 0u32;
749            // SAFETY: the buffer is writable for the length passed; the handle has query access.
750            let status = unsafe {
751                NtQueryInformationProcess(
752                    process.0 .0,
753                    PROCESS_COMMAND_LINE_INFORMATION,
754                    buffer.as_mut_ptr().cast(),
755                    (buffer.len() * 8) as u32,
756                    &mut returned,
757                )
758            } as u32;
759            match status {
760                0 => return Ok(read_unicode_string(&buffer)),
761                STATUS_INFO_LENGTH_MISMATCH | STATUS_BUFFER_OVERFLOW | STATUS_BUFFER_TOO_SMALL => {
762                    size = (returned as usize).max(size * 2);
763                }
764                _ => return Ok(None), // denied, protected or exiting: unknown, never a match
765            }
766        }
767        Ok(None)
768    }
769
770    fn read_unicode_string(buffer: &[u64]) -> Option<Vec<OsString>> {
771        let base = buffer.as_ptr() as usize;
772        let end = base + buffer.len() * 8;
773        if buffer.len() * 8 < std::mem::size_of::<UnicodeString>() {
774            return None;
775        }
776        // SAFETY: the buffer is aligned for u64 and large enough for the header.
777        let header = unsafe { &*(buffer.as_ptr().cast::<UnicodeString>()) };
778        let text = header.buffer as usize;
779        let bytes = usize::from(header.length);
780        if bytes == 0 {
781            return Some(Vec::new());
782        }
783        // The text must lie inside the buffer, after the header, and be properly aligned.
784        if text < base + std::mem::size_of::<UnicodeString>()
785            || !text.is_multiple_of(2)
786            || !bytes.is_multiple_of(2)
787            || text + bytes > end
788        {
789            return None;
790        }
791        // SAFETY: the range was checked to lie inside the buffer and to be 2-byte aligned.
792        let units = unsafe { std::slice::from_raw_parts(header.buffer, bytes / 2) };
793        let line = String::from_utf16_lossy(units);
794        Some(
795            super::split_windows_command_line(&line)
796                .into_iter()
797                .map(OsString::from)
798                .collect(),
799        )
800    }
801}
802
803#[cfg(not(any(
804    target_os = "linux",
805    target_os = "android",
806    target_os = "macos",
807    windows
808)))]
809mod os {
810    use std::{ffi::OsString, io};
811
812    pub(super) struct Me;
813
814    impl Me {
815        pub(super) fn current() -> io::Result<Self> {
816            Err(io::Error::from(io::ErrorKind::Unsupported))
817        }
818
819        pub(super) fn owns(&self, _pid: u32) -> io::Result<bool> {
820            Err(io::Error::from(io::ErrorKind::Unsupported))
821        }
822    }
823
824    pub(super) fn command_line(_pid: u32) -> io::Result<Option<Vec<OsString>>> {
825        Err(io::Error::from(io::ErrorKind::Unsupported))
826    }
827}
828
829#[cfg(test)]
830mod tests {
831    use super::*;
832
833    fn strings(parts: &[&str]) -> Vec<OsString> {
834        parts.iter().map(OsString::from).collect()
835    }
836
837    #[test]
838    fn an_empty_query_and_empty_criteria_are_refused() {
839        for query in [
840            ProcessQuery::new(),
841            ProcessQuery::new().exe(""),
842            ProcessQuery::new().arg(""),
843            ProcessQuery::new().arg_containing(""),
844        ] {
845            let error = find_processes(&query).unwrap_err();
846            assert_eq!(error.kind(), io::ErrorKind::InvalidInput, "{query:?}");
847        }
848    }
849
850    #[test]
851    fn arguments_match_exactly_and_by_substring_and_all_criteria_must_hold() {
852        let args = strings(&["chrome", "--user-data-dir=/p/rk-profile-1", "--flag"]);
853        let both = ProcessQuery::new()
854            .arg("--flag")
855            .arg_containing("rk-profile-1");
856        assert!(args_match(&both, &args));
857        assert!(!args_match(&ProcessQuery::new().arg("--fla"), &args));
858        assert!(!args_match(
859            &ProcessQuery::new().arg_containing("rk-profile-2"),
860            &args
861        ));
862        assert!(!args_match(&both.arg("--missing"), &args));
863    }
864
865    #[test]
866    fn a_substring_is_found_inside_one_argument_not_across_two() {
867        let args = strings(&["tool", "ab", "cd"]);
868        assert!(args_match(&ProcessQuery::new().arg_containing("ab"), &args));
869        assert!(!args_match(
870            &ProcessQuery::new().arg_containing("b c"),
871            &args
872        ));
873        assert!(!args_match(
874            &ProcessQuery::new().arg_containing("bcd"),
875            &args
876        ));
877    }
878
879    #[test]
880    fn ancestors_follow_the_parent_chain_and_stop_on_a_loop() {
881        let table = [(10, 5), (5, 2), (2, 1), (1, 0), (7, 9), (9, 7)];
882        assert_eq!(ancestors(&table, 10), HashSet::from([5, 2, 1]));
883        assert_eq!(ancestors(&table, 7), HashSet::from([9]));
884        assert!(ancestors(&table, 99).is_empty());
885    }
886
887    #[test]
888    fn windows_command_lines_split_like_the_c_runtime() {
889        assert_eq!(
890            split_windows_command_line(r#""C:\Program Files\x.exe" a "b c" d\"e "f\\" g"#),
891            vec![r"C:\Program Files\x.exe", "a", "b c", "d\"e", r"f\", "g"]
892        );
893        assert_eq!(
894            split_windows_command_line("prog   one\ttwo"),
895            vec!["prog", "one", "two"]
896        );
897        assert_eq!(split_windows_command_line(r#"p "" x"#), vec!["p", "", "x"]);
898        assert_eq!(
899            split_windows_command_line(r#"p "a""b" c"#),
900            vec!["p", "a\"b", "c"]
901        );
902        assert_eq!(split_windows_command_line(r"p a\\b"), vec!["p", r"a\\b"]);
903        assert!(split_windows_command_line("   ").is_empty());
904    }
905
906    #[cfg(unix)]
907    #[test]
908    fn procargs2_yields_the_arguments_and_not_the_environment() {
909        let mut buffer = 3i32.to_ne_bytes().to_vec();
910        buffer.extend_from_slice(b"/bin/sh\0\0\0sh\0-c\0read x\0HOME=/root\0");
911        assert_eq!(
912            parse_procargs2(&buffer).unwrap(),
913            strings(&["sh", "-c", "read x"])
914        );
915        // Cut short in the middle of the last argument: not a partial match.
916        let mut short = 3i32.to_ne_bytes().to_vec();
917        short.extend_from_slice(b"/bin/sh\0\0sh\0-c\0rea");
918        assert!(parse_procargs2(&short).is_none());
919        assert!(parse_procargs2(&[1, 0]).is_none());
920    }
921
922    #[cfg(windows)]
923    #[test]
924    fn windows_paths_compare_without_case_or_slash_style() {
925        assert!(same_path(
926            Path::new(r"C:\Program Files\App\x.exe"),
927            Path::new("c:/program files/app/X.EXE")
928        ));
929        assert!(!same_path(
930            Path::new(r"C:\a\x.exe"),
931            Path::new(r"C:\b\x.exe")
932        ));
933    }
934}