Skip to main content

rightkit_daemon/
host_identity.rs

1//! Verifying the HOST from the client side.
2//!
3//! The server side of this crate has always checked its peer: the kernel says the client runs as
4//! the same user. A client used to check only the mirror image of that, that whatever answered at
5//! the recorded endpoint also runs as the same user. That says nothing about WHICH program it is,
6//! so a program that managed to get the client to dial it (by redirecting the suite root, by
7//! writing a `service.json`, or by binding the socket path first) received the client's `hello`
8//! and every request after it, including any secret in them.
9//!
10//! [`HostExpectation`] closes that. The caller names the executable(s) the host may be;
11//! [`Client::connect_expecting`](crate::Client::connect_expecting) asks the kernel who is on the
12//! listening end of the connected socket and refuses anyone else before a single application
13//! byte is written. See the README section "Verifying the host" for the threat model.
14
15use std::ffi::OsStr;
16use std::fmt;
17use std::io;
18use std::path::{Path, PathBuf};
19use std::sync::Arc;
20
21/// The process the OS says is on the listening end of a connection this client opened, as it was
22/// when every check passed. Returned by
23/// [`Client::verified_host`](crate::Client::verified_host) and handed to a caller-supplied
24/// verifier ([`HostExpectation::with_verifier`]).
25#[derive(Debug, Clone, PartialEq, Eq)]
26#[non_exhaustive]
27pub struct HostProcess {
28    /// The host's process id as the kernel recorded it for the socket. A pid names a process
29    /// only while it runs: use it for an audit trail, not as a credential.
30    pub pid: u32,
31    /// Unix: the host's effective uid (always the client's own). Windows: `None`.
32    pub uid: Option<u32>,
33    /// Windows: the host process token's user SID. Unix: `None`.
34    pub sid: Option<String>,
35    /// The host's executable path as the kernel reports it for `pid` (macOS `proc_pidpath`,
36    /// Linux `/proc/<pid>/exe`), canonicalised when it still exists on disk.
37    pub executable: PathBuf,
38    /// macOS: the host's audit token (`audit_token_t`, eight words: pid at index 5, pid version
39    /// at index 7) from `LOCAL_PEERTOKEN`, when the kernel provides it. It is the race-free
40    /// handle for code-signature checks (`SecCodeCopyGuestWithAttributes` with
41    /// `kSecGuestAttributeAudit`), which a [`HostExpectation::with_verifier`] closure can use.
42    /// `None` elsewhere.
43    pub audit_token: Option<[u32; 8]>,
44}
45
46/// Why a host was refused. Always the host's identity, never a connection problem.
47#[derive(Debug, Clone, PartialEq, Eq)]
48#[non_exhaustive]
49pub enum HostIdentityReason {
50    /// The expectation names no executable, so no host can match. Fails closed.
51    NoExpectation,
52    /// The host runs as another user than this client.
53    UidMismatch { host: Option<u32>, client: u32 },
54    /// The OS did not report the host's process id (a BSD, or a pid namespace that hides it).
55    PidUnavailable,
56    /// The kernel would not describe the host process (permission, or the call failed).
57    Unreadable(String),
58    /// The host's executable is not one of the expected executables.
59    ExecutableMismatch,
60    /// The process behind the socket exited or changed while it was being identified, so the pid
61    /// could not be proven to name one process throughout.
62    HostChanged(String),
63    /// The executable matched but the caller's extra verification refused the host.
64    Refused(String),
65    /// Identifying the host is not implemented for this platform. Fails closed: it never means
66    /// "assume it is the right one".
67    Unverified {
68        platform: &'static str,
69        detail: &'static str,
70    },
71    /// The OS could not answer the peer-credential query.
72    Query(String),
73    /// The caller is not a local app (a remote device has no OS peer), so there is no process
74    /// to identify. Fails closed.
75    NotLocalPeer,
76}
77
78impl fmt::Display for HostIdentityReason {
79    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80        match self {
81            HostIdentityReason::NoExpectation => {
82                write!(
83                    f,
84                    "no expected host executable was supplied, so no host can match"
85                )
86            }
87            HostIdentityReason::UidMismatch { host, client } => match host {
88                Some(host) => write!(
89                    f,
90                    "the host runs as uid {host}, this client as uid {client}"
91                ),
92                None => write!(
93                    f,
94                    "the host's uid is unknown, this client runs as uid {client}"
95                ),
96            },
97            HostIdentityReason::PidUnavailable => {
98                write!(f, "the OS did not report the host's process id")
99            }
100            HostIdentityReason::Unreadable(why) => {
101                write!(f, "the OS would not describe the host process ({why})")
102            }
103            HostIdentityReason::ExecutableMismatch => {
104                write!(
105                    f,
106                    "the host's executable is not one of the expected executables"
107                )
108            }
109            HostIdentityReason::HostChanged(why) => {
110                write!(
111                    f,
112                    "the host process changed while it was identified ({why})"
113                )
114            }
115            HostIdentityReason::Refused(why) => {
116                write!(f, "the caller's extra host verification refused it ({why})")
117            }
118            HostIdentityReason::Unverified { platform, detail } => {
119                write!(
120                    f,
121                    "host verification is unavailable on {platform} ({detail})"
122                )
123            }
124            HostIdentityReason::Query(why) => {
125                write!(f, "the OS could not identify the peer ({why})")
126            }
127            HostIdentityReason::NotLocalPeer => {
128                write!(
129                    f,
130                    "the caller is not a local app, so it has no process to identify"
131                )
132            }
133        }
134    }
135}
136
137/// The process behind the endpoint is not the host the caller expected. No `hello` and no request
138/// frame was written to it.
139///
140/// Distinct from a connection error: something answered, and the OS says it is the wrong program.
141/// It is not retryable (reconnecting reaches the same program). Content-free: pids and paths
142/// only, never request data.
143#[derive(Debug, Clone, PartialEq, Eq)]
144#[non_exhaustive]
145pub struct HostIdentityError {
146    /// The executables the caller said the host may be, as supplied (not canonicalised).
147    pub expected: Vec<PathBuf>,
148    /// The pid the kernel reported for the host, when it reported one.
149    pub found_pid: Option<u32>,
150    /// The host's executable, when it could be read before the refusal.
151    pub found_executable: Option<PathBuf>,
152    pub reason: HostIdentityReason,
153}
154
155impl fmt::Display for HostIdentityError {
156    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
157        write!(
158            f,
159            "the service host failed identity verification: {}",
160            self.reason
161        )?;
162        if let Some(pid) = self.found_pid {
163            write!(f, "; host pid {pid}")?;
164        }
165        if let Some(exe) = &self.found_executable {
166            write!(f, "; host executable {}", exe.display())?;
167        }
168        if self.expected.is_empty() {
169            write!(f, "; expected: none supplied")
170        } else {
171            write!(f, "; expected ")?;
172            for (i, path) in self.expected.iter().enumerate() {
173                if i > 0 {
174                    write!(f, " or ")?;
175                }
176                write!(f, "{}", path.display())?;
177            }
178            Ok(())
179        }
180    }
181}
182
183impl std::error::Error for HostIdentityError {}
184
185/// Internal: why opening a host-verified connection failed.
186#[derive(Debug)]
187pub(crate) enum HostConnectError {
188    /// The connection itself failed, or the host left before it could be identified.
189    Io(io::Error),
190    /// The host answered and is not the expected one.
191    Identity(HostIdentityError),
192}
193
194type Verifier = Arc<dyn Fn(&HostProcess) -> Result<(), String> + Send + Sync>;
195
196/// Which program the client is willing to talk to, passed to
197/// [`Client::connect_expecting`](crate::Client::connect_expecting).
198///
199/// The host must run as the client's own user (always) and its executable, read from the kernel
200/// for the connected socket's process, must be one of the paths named here. Both sides of the
201/// comparison are canonicalised when the check runs, so a symlink, a `..` or a case difference on
202/// a case-insensitive volume does not matter; the same file reached by another name (a hard link)
203/// is the same executable.
204///
205/// An expectation with no path matches nothing: it fails closed.
206///
207/// The install-relative constructors ([`sibling_executable`](Self::sibling_executable),
208/// [`bundle_executable`](Self::bundle_executable),
209/// [`relative_to_current_exe`](Self::relative_to_current_exe)) derive the host's path from where
210/// THIS process's executable lives, so an engine shipped inside an app can name its app without a
211/// hard-coded path. They inherit the trust of that location: see the README, "What this does not
212/// do".
213#[derive(Clone, Default)]
214pub struct HostExpectation {
215    executables: Vec<PathBuf>,
216    verifier: Option<Verifier>,
217    /// The server-side check accepts any executable and relies on the verifier alone (a peer
218    /// admitted by code signature, see `peer_signature`). Never set by a public constructor that
219    /// a caller can reach without also naming a verifier.
220    any_executable: bool,
221}
222
223impl fmt::Debug for HostExpectation {
224    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
225        f.debug_struct("HostExpectation")
226            .field("executables", &self.executables)
227            .field("verifier", &self.verifier.as_ref().map(|_| "<fn>"))
228            .finish()
229    }
230}
231
232fn invalid(what: &str) -> io::Error {
233    io::Error::new(io::ErrorKind::InvalidInput, what.to_string())
234}
235
236fn current_exe_canonical() -> io::Result<PathBuf> {
237    std::fs::canonicalize(std::env::current_exe()?)
238}
239
240/// A bare file name: one normal component, no separators, no `..`.
241fn is_single_component(name: &OsStr) -> bool {
242    let path = Path::new(name);
243    path.file_name() == Some(name) && path.components().count() == 1
244}
245
246/// `<ancestor of exe, levels_up levels above it>/<relative>`. `levels_up` 1 is the directory that
247/// holds `exe`. `None` for 0 levels, a path with no such ancestor, or a `relative` that is not
248/// relative: absolute, or (Windows) rooted without a drive (`\x`, `/x`) or carrying a drive
249/// (`C:x`), each of which `join` would let replace the base.
250fn up_and_join(exe: &Path, levels_up: usize, relative: &Path) -> Option<PathBuf> {
251    use std::path::Component;
252    let leaves_base = relative
253        .components()
254        .any(|part| matches!(part, Component::Prefix(_) | Component::RootDir));
255    if levels_up == 0 || relative.is_absolute() || leaves_base {
256        return None;
257    }
258    exe.ancestors()
259        .nth(levels_up)
260        .map(|base| base.join(relative))
261}
262
263/// `<enclosing>.app/Contents/MacOS/<file_name>` for the first `.app` directory above `exe`.
264fn bundle_main(exe: &Path, file_name: &OsStr) -> Option<PathBuf> {
265    exe.ancestors()
266        .skip(1)
267        .find(|dir| dir.extension() == Some(OsStr::new("app")))
268        .map(|bundle| bundle.join("Contents").join("MacOS").join(file_name))
269}
270
271impl HostExpectation {
272    /// The host is the program at `path`.
273    pub fn executable(path: impl Into<PathBuf>) -> Self {
274        Self {
275            executables: vec![path.into()],
276            verifier: None,
277            any_executable: false,
278        }
279    }
280
281    /// The host is any one of `paths` (a release path and a QA path, say). Empty matches nothing.
282    pub fn any_of<I, P>(paths: I) -> Self
283    where
284        I: IntoIterator<Item = P>,
285        P: Into<PathBuf>,
286    {
287        Self {
288            executables: paths.into_iter().map(Into::into).collect(),
289            verifier: None,
290            any_executable: false,
291        }
292    }
293
294    /// Also accept the program at `path`.
295    pub fn or_executable(mut self, path: impl Into<PathBuf>) -> Self {
296        self.executables.push(path.into());
297        self
298    }
299
300    /// The host is the executable `file_name` in the same directory as THIS process's executable
301    /// (symlinks resolved first). Fails when the running executable cannot be located or
302    /// `file_name` is not a bare file name.
303    pub fn sibling_executable(file_name: impl AsRef<OsStr>) -> io::Result<Self> {
304        let name = file_name.as_ref();
305        if !is_single_component(name) {
306            return Err(invalid("the host file name must be a bare file name"));
307        }
308        Self::relative_to_current_exe(1, name)
309    }
310
311    /// The host is the main executable `file_name` of the macOS app bundle that contains THIS
312    /// process's executable: `<App>.app/Contents/MacOS/<file_name>`. For an engine or command
313    /// line tool shipped inside the app, whether it sits in `Contents/Resources/bin` or in
314    /// `Contents/MacOS` itself. Fails when the running executable is not inside an `.app`
315    /// directory or `file_name` is not a bare file name.
316    pub fn bundle_executable(file_name: impl AsRef<OsStr>) -> io::Result<Self> {
317        let name = file_name.as_ref();
318        if !is_single_component(name) {
319            return Err(invalid("the host file name must be a bare file name"));
320        }
321        let exe = current_exe_canonical()?;
322        let path = bundle_main(&exe, name).ok_or_else(|| {
323            io::Error::new(
324                io::ErrorKind::NotFound,
325                "the running executable is not inside an .app bundle",
326            )
327        })?;
328        Ok(Self::executable(path))
329    }
330
331    /// The host is `relative` under the directory `levels_up` levels above THIS process's
332    /// executable (symlinks resolved first; 1 is the directory that holds it). For install
333    /// layouts that are neither a sibling nor a macOS bundle, for example
334    /// `relative_to_current_exe(2, "bin/app")` for `<root>/libexec/engine` and `<root>/bin/app`.
335    pub fn relative_to_current_exe(
336        levels_up: usize,
337        relative: impl AsRef<Path>,
338    ) -> io::Result<Self> {
339        let exe = current_exe_canonical()?;
340        let path = up_and_join(&exe, levels_up, relative.as_ref()).ok_or_else(|| {
341            invalid("no such directory above the running executable, or the path is absolute")
342        })?;
343        Ok(Self::executable(path))
344    }
345
346    /// Add a check that runs AFTER the executable matched, with the host the OS identified. A
347    /// returned `Err(reason)` refuses the host as [`HostIdentityReason::Refused`]. The check runs
348    /// before the first application byte is written.
349    ///
350    /// This is the hook for a stronger identity than a path. This crate links no code-signing API,
351    /// so the macOS team-identifier check is the caller's: link Security.framework in the app,
352    /// build a `SecCode` for the host from [`HostProcess::audit_token`] with
353    /// `SecCodeCopyGuestWithAttributes` and `kSecGuestAttributeAudit` (never from the pid alone,
354    /// which can be reused), read `kSecCodeInfoTeamIdentifier` with `SecCodeCopySigningInformation`
355    /// and compare it with your own (`SecCodeCopySelf`). On Windows the same hook carries an
356    /// Authenticode (`WinVerifyTrust`) check of [`HostProcess::executable`].
357    pub fn with_verifier(
358        mut self,
359        check: impl Fn(&HostProcess) -> Result<(), String> + Send + Sync + 'static,
360    ) -> Self {
361        self.verifier = Some(Arc::new(check));
362        self
363    }
364
365    /// The executables this expectation names, as supplied.
366    pub fn executables(&self) -> &[PathBuf] {
367        &self.executables
368    }
369
370    /// An expectation that names no path: the server-side check identifies the peer process
371    /// (same user, pid pinned by start time) and accepts it only when `check` does. Crate-internal
372    /// so that "any executable" is always paired with a verifier (a code-signature check).
373    pub(crate) fn any_executable_verified(
374        check: impl Fn(&HostProcess) -> Result<(), String> + Send + Sync + 'static,
375    ) -> Self {
376        Self {
377            executables: Vec::new(),
378            verifier: Some(Arc::new(check)),
379            any_executable: true,
380        }
381    }
382}
383
384/// Server side of [`HostExpectation`]: does the process behind `peer` (the OS-verified identity
385/// of a local caller) run the program `expect` names? `None` (a caller with no OS peer) and any
386/// platform that cannot answer are refused. Used by `CallContext::verify_peer_executable`.
387pub(crate) fn verify_peer_process(
388    peer: Option<&crate::peer::PeerIdentity>,
389    expect: &HostExpectation,
390) -> Result<HostProcess, HostIdentityError> {
391    let Some(peer) = peer else {
392        return Err(HostIdentityError {
393            expected: expect.executables.clone(),
394            found_pid: None,
395            found_executable: None,
396            reason: HostIdentityReason::NotLocalPeer,
397        });
398    };
399    #[cfg(unix)]
400    {
401        verify_unix_pid(peer, expect)
402    }
403    #[cfg(windows)]
404    {
405        verify_pid_host(peer, expect)
406    }
407    #[cfg(not(any(unix, windows)))]
408    {
409        Err(HostIdentityError {
410            expected: expect.executables.clone(),
411            found_pid: peer.pid,
412            found_executable: None,
413            reason: HostIdentityReason::Unverified {
414                platform: std::env::consts::OS,
415                detail: "no process identification is implemented for this platform",
416            },
417        })
418    }
419}
420
421/// The start marker of process `pid` as the OS reports it now (`None` when unreadable or gone).
422/// Recorded at accept into `PeerIdentity::started`, compared by the server-side executable check.
423pub(crate) fn process_start_marker(pid: u32) -> Option<(u64, u64)> {
424    #[cfg(unix)]
425    {
426        unix_impl::start_marker_of(pid)
427    }
428    #[cfg(windows)]
429    {
430        windows_impl::start_marker_of(pid)
431    }
432    #[cfg(not(any(unix, windows)))]
433    {
434        let _ = pid;
435        None
436    }
437}
438
439#[cfg(unix)]
440pub(crate) use unix_impl::{verify_unix_host, verify_unix_pid};
441// Read only by peer.rs's getpeereid branch (the BSDs and Apple platforms).
442#[cfg(all(
443    unix,
444    any(
445        target_os = "macos",
446        target_os = "ios",
447        target_os = "freebsd",
448        target_os = "openbsd",
449        target_os = "netbsd",
450        target_os = "dragonfly"
451    )
452))]
453pub(crate) use unix_impl::peer_audit_token;
454
455#[cfg(unix)]
456mod unix_impl {
457    use super::{
458        HostConnectError, HostExpectation, HostIdentityError, HostIdentityReason, HostProcess,
459    };
460    use crate::peer::{current_euid, verify_peer_is_current_user, PeerError, PeerIdentity};
461    use std::io;
462    use std::os::unix::fs::MetadataExt;
463    use std::os::unix::net::UnixStream;
464    use std::path::{Path, PathBuf};
465
466    /// `(device, inode)`: names one file whatever path it is reached by.
467    pub type FileId = (u64, u64);
468
469    pub fn file_id(path: &Path) -> Option<FileId> {
470        let md = std::fs::metadata(path).ok()?;
471        Some((md.dev(), md.ino()))
472    }
473
474    /// A running process's executable as the kernel reports it.
475    pub struct ExeInfo {
476        pub path: PathBuf,
477        /// The file the process is actually running, when the kernel lets us stat it (Linux:
478        /// through `/proc/<pid>/exe`, exact even if the file was replaced; macOS: by path).
479        pub id: Option<FileId>,
480    }
481
482    /// What must be unchanged across the executable-path read to prove the pid named one process
483    /// throughout: its start time, and (macOS) its audit token, which carries a pid version.
484    #[derive(PartialEq, Eq)]
485    struct Witness {
486        start: (u64, u64),
487        token: Option<[u32; 8]>,
488    }
489
490    enum ObserveError {
491        Gone,
492        Unreadable(String),
493    }
494
495    impl From<ObserveError> for HostIdentityReason {
496        fn from(e: ObserveError) -> Self {
497            match e {
498                ObserveError::Gone => HostIdentityReason::HostChanged(
499                    "it exited before it could be identified".to_string(),
500                ),
501                ObserveError::Unreadable(why) => HostIdentityReason::Unreadable(why),
502            }
503        }
504    }
505
506    /// macOS: the errno that says "no such process" (ESRCH, 3), or a stale zero, is `Ok(None)`;
507    /// anything else is an error. Read right after a `proc_*` call that returned 0.
508    #[cfg(target_os = "macos")]
509    fn missing_or_err<T>() -> io::Result<Option<T>> {
510        let e = io::Error::last_os_error();
511        match e.raw_os_error() {
512            Some(3) | Some(0) | None => Ok(None),
513            _ => Err(e),
514        }
515    }
516
517    fn witness(stream: &UnixStream, pid: u32) -> Result<Witness, ObserveError> {
518        let token = os::audit_token(stream);
519        let start = match os::start_marker(pid) {
520            Ok(Some(start)) => start,
521            Ok(None) => return Err(ObserveError::Gone),
522            Err(e) => return Err(ObserveError::Unreadable(format!("process start time: {e}"))),
523        };
524        Ok(Witness { start, token })
525    }
526
527    /// Is the running executable (`found`, canonical, with its file identity `found_id`) the one
528    /// `expected` names? Equal after canonicalisation, or the very same file (device and inode),
529    /// which covers a case-insensitive volume (macOS `realpath` does not normalise case, so
530    /// `/applications/x` and `/Applications/x` differ as strings) and a hard link. An expected
531    /// path that does not exist matches nothing.
532    pub fn same_executable(expected: &Path, found: &Path, found_id: Option<FileId>) -> bool {
533        let Ok(canonical) = std::fs::canonicalize(expected) else {
534            return false;
535        };
536        if canonical == found {
537            return true;
538        }
539        match (found_id, file_id(&canonical)) {
540            (Some(running), Some(named)) => running == named,
541            _ => false,
542        }
543    }
544
545    /// Ask the kernel who is on the listening end of `stream` and decide whether it is the host
546    /// `expect` names. Reads and writes nothing on the stream.
547    ///
548    /// Order, each step failing closed:
549    /// 1. the peer's uid equals ours (`getpeereid` / `SO_PEERCRED`, the existing same-user check);
550    /// 2. the pid the kernel reports for the socket (`LOCAL_PEERPID` / `SO_PEERCRED`);
551    /// 3. the pid's start time (and, on macOS, the audit token), then its executable path
552    ///    (`proc_pidpath` / `/proc/<pid>/exe`), then the start time and token again, then the pid
553    ///    from the socket again: all must agree, or the pid cannot be proven to name one process;
554    /// 4. the executable is one of the expected executables;
555    /// 5. the caller's extra verifier, if any.
556    pub(crate) fn verify_unix_host(
557        stream: &UnixStream,
558        expect: &HostExpectation,
559    ) -> Result<HostProcess, HostConnectError> {
560        let refuse = |pid: Option<u32>, exe: Option<PathBuf>, reason: HostIdentityReason| {
561            HostConnectError::Identity(HostIdentityError {
562                expected: expect.executables.clone(),
563                found_pid: pid,
564                found_executable: exe,
565                reason,
566            })
567        };
568        if expect.executables.is_empty() {
569            return Err(refuse(None, None, HostIdentityReason::NoExpectation));
570        }
571        if !os::SUPPORTED {
572            return Err(refuse(
573                None,
574                None,
575                HostIdentityReason::Unverified {
576                    platform: std::env::consts::OS,
577                    detail: "reading a process's executable path is not implemented for this Unix",
578                },
579            ));
580        }
581
582        let first = match verify_peer_is_current_user(stream) {
583            Ok(id) => id,
584            Err(PeerError::Mismatch(id)) => {
585                return Err(refuse(
586                    id.pid,
587                    None,
588                    HostIdentityReason::UidMismatch {
589                        host: id.uid,
590                        client: current_euid(),
591                    },
592                ))
593            }
594            Err(PeerError::Unsupported(detail)) => {
595                return Err(refuse(
596                    None,
597                    None,
598                    HostIdentityReason::Unverified {
599                        platform: std::env::consts::OS,
600                        detail,
601                    },
602                ))
603            }
604            Err(e) => {
605                // The host hung up before it could be identified: a connection problem.
606                if e.peer_gone() {
607                    return Err(HostConnectError::Io(io::Error::from(e)));
608                }
609                return Err(refuse(None, None, HostIdentityReason::Query(e.to_string())));
610            }
611        };
612        let Some(pid) = first.pid else {
613            return Err(refuse(None, None, HostIdentityReason::PidUnavailable));
614        };
615
616        let observe = |e: ObserveError| refuse(Some(pid), None, e.into());
617        let changed = |why: &str| {
618            refuse(
619                Some(pid),
620                None,
621                HostIdentityReason::HostChanged(why.to_string()),
622            )
623        };
624
625        let before = witness(stream, pid).map_err(observe)?;
626        let exe = match os::executable_of(pid) {
627            Ok(Some(exe)) => exe,
628            Ok(None) => return Err(observe(ObserveError::Gone)),
629            Err(e) => {
630                return Err(observe(ObserveError::Unreadable(format!(
631                    "executable path: {e}"
632                ))))
633            }
634        };
635        let after = witness(stream, pid).map_err(observe)?;
636        if before != after {
637            return Err(changed(
638                "its identity differed before and after its executable path was read",
639            ));
640        }
641        match verify_peer_is_current_user(stream) {
642            Ok(again) if again.pid == Some(pid) => {}
643            _ => return Err(changed("the socket no longer reports the same process id")),
644        }
645
646        conclude(expect, pid, exe, after.token).map_err(HostConnectError::Identity)
647    }
648
649    /// The last steps, shared by the client-side and the server-side check: the process's
650    /// executable (canonicalised) must be one of the expected executables, then the caller's
651    /// extra verifier, if any, must accept the process.
652    fn conclude(
653        expect: &HostExpectation,
654        pid: u32,
655        exe: ExeInfo,
656        audit_token: Option<[u32; 8]>,
657    ) -> Result<HostProcess, HostIdentityError> {
658        let refuse = |exe: Option<PathBuf>, reason: HostIdentityReason| HostIdentityError {
659            expected: expect.executables.clone(),
660            found_pid: Some(pid),
661            found_executable: exe,
662            reason,
663        };
664        let ExeInfo { path, id } = exe;
665        let found = std::fs::canonicalize(&path).unwrap_or(path);
666        if !expect.any_executable
667            && !expect
668                .executables
669                .iter()
670                .any(|expected| same_executable(expected, &found, id))
671        {
672            return Err(refuse(Some(found), HostIdentityReason::ExecutableMismatch));
673        }
674
675        let host = HostProcess {
676            pid,
677            uid: Some(current_euid()),
678            sid: None,
679            executable: found,
680            audit_token,
681        };
682        if let Some(verifier) = &expect.verifier {
683            if let Err(why) = verifier(&host) {
684                return Err(refuse(
685                    Some(host.executable.clone()),
686                    HostIdentityReason::Refused(why),
687                ));
688            }
689        }
690        Ok(host)
691    }
692
693    /// Server side: identify the process that opened a connection, from the OS-verified
694    /// [`PeerIdentity`] recorded when it connected, and decide whether it is the program
695    /// `expect` names. Same decision as [`verify_unix_host`] (same executable comparison, same
696    /// verifier), but the socket is no longer at hand, so the process is pinned by its pid, its
697    /// start time read before and after its executable path, and (macOS) the audit token the
698    /// kernel captured at connect time.
699    ///
700    /// Each step fails closed: no uid or a different uid, no pid, a process that is gone, a
701    /// start time that changed, an audit token that names another pid, an unreadable executable,
702    /// no expected executable, an executable that is not expected, a refusing verifier.
703    pub(crate) fn verify_unix_pid(
704        peer: &PeerIdentity,
705        expect: &HostExpectation,
706    ) -> Result<HostProcess, HostIdentityError> {
707        let refuse = |pid: Option<u32>, exe: Option<PathBuf>, reason: HostIdentityReason| {
708            HostIdentityError {
709                expected: expect.executables.clone(),
710                found_pid: pid,
711                found_executable: exe,
712                reason,
713            }
714        };
715        if expect.executables.is_empty() && !expect.any_executable {
716            return Err(refuse(None, None, HostIdentityReason::NoExpectation));
717        }
718        if !os::SUPPORTED {
719            return Err(refuse(
720                None,
721                None,
722                HostIdentityReason::Unverified {
723                    platform: std::env::consts::OS,
724                    detail: "reading a process's executable path is not implemented for this Unix",
725                },
726            ));
727        }
728        if peer.uid != Some(current_euid()) {
729            return Err(refuse(
730                peer.pid,
731                None,
732                HostIdentityReason::UidMismatch {
733                    host: peer.uid,
734                    client: current_euid(),
735                },
736            ));
737        }
738        let Some(pid) = peer.pid else {
739            return Err(refuse(None, None, HostIdentityReason::PidUnavailable));
740        };
741        if let Some(token) = peer.audit_token {
742            // `audit_token_t`: the pid is word 5.
743            if token[5] != pid {
744                return Err(refuse(
745                    Some(pid),
746                    None,
747                    HostIdentityReason::HostChanged(
748                        "the audit token and the process id name different processes".to_string(),
749                    ),
750                ));
751            }
752        }
753        let observe = |e: ObserveError| refuse(Some(pid), None, e.into());
754        let start = |pid| match os::start_marker(pid) {
755            Ok(Some(start)) => Ok(start),
756            Ok(None) => Err(ObserveError::Gone),
757            Err(e) => Err(ObserveError::Unreadable(format!("process start time: {e}"))),
758        };
759        let before = start(pid).map_err(observe)?;
760        // The pid must still be the process that connected: its start time was recorded when
761        // the connection was accepted. No record, or a different one, is a pid that may have
762        // been reused. Fails closed.
763        if peer.started != Some(before) {
764            return Err(refuse(
765                Some(pid),
766                None,
767                HostIdentityReason::HostChanged(
768                    concat!(
769                        "the process is not the one that connected (its start time differs ",
770                        "from the one recorded at connect, or none was recorded)"
771                    )
772                    .to_string(),
773                ),
774            ));
775        }
776        let exe = match os::executable_of(pid) {
777            Ok(Some(exe)) => exe,
778            Ok(None) => return Err(observe(ObserveError::Gone)),
779            Err(e) => {
780                return Err(observe(ObserveError::Unreadable(format!(
781                    "executable path: {e}"
782                ))))
783            }
784        };
785        let after = start(pid).map_err(observe)?;
786        if before != after {
787            return Err(refuse(
788                Some(pid),
789                None,
790                HostIdentityReason::HostChanged(
791                    "its start time differed before and after its executable path was read"
792                        .to_string(),
793                ),
794            ));
795        }
796        conclude(expect, pid, exe, peer.audit_token)
797    }
798
799    pub(crate) fn start_marker_of(pid: u32) -> Option<(u64, u64)> {
800        os::start_marker(pid).ok().flatten()
801    }
802
803    /// The peer's audit token for [`PeerIdentity::audit_token`]; `None` off macOS.
804    #[cfg(any(
805        target_os = "macos",
806        target_os = "ios",
807        target_os = "freebsd",
808        target_os = "openbsd",
809        target_os = "netbsd",
810        target_os = "dragonfly"
811    ))]
812    pub(crate) fn peer_audit_token(stream: &UnixStream) -> Option<[u32; 8]> {
813        os::audit_token(stream)
814    }
815
816    #[cfg(target_os = "macos")]
817    mod os {
818        use super::{file_id, missing_or_err, ExeInfo};
819        use std::ffi::{c_void, OsString};
820        use std::io;
821        use std::os::fd::AsRawFd;
822        use std::os::unix::ffi::OsStringExt;
823        use std::os::unix::net::UnixStream;
824        use std::path::PathBuf;
825
826        pub const SUPPORTED: bool = true;
827
828        /// `struct proc_bsdinfo` from `<sys/proc_info.h>`. Checked on macOS (Darwin 27): 136 bytes,
829        /// `pbi_pid` at offset 12, `pbi_start_tvsec` at offset 120.
830        #[repr(C)]
831        #[allow(dead_code)]
832        struct ProcBsdInfo {
833            pbi_flags: u32,
834            pbi_status: u32,
835            pbi_xstatus: u32,
836            pbi_pid: u32,
837            pbi_ppid: u32,
838            pbi_uid: u32,
839            pbi_gid: u32,
840            pbi_ruid: u32,
841            pbi_rgid: u32,
842            pbi_svuid: u32,
843            pbi_svgid: u32,
844            rfu_1: u32,
845            pbi_comm: [u8; 16],
846            pbi_name: [u8; 32],
847            pbi_nfiles: u32,
848            pbi_pgid: u32,
849            pbi_pjobc: u32,
850            e_tdev: u32,
851            e_tpgid: u32,
852            pbi_nice: i32,
853            pbi_start_tvsec: u64,
854            pbi_start_tvusec: u64,
855        }
856        const _: () = assert!(std::mem::size_of::<ProcBsdInfo>() == 136);
857
858        const PROC_PIDTBSDINFO: i32 = 3;
859        const PROC_PIDPATHINFO_MAXSIZE: usize = 4 * 1024;
860        const SOL_LOCAL: i32 = 0;
861        const LOCAL_PEERTOKEN: i32 = 0x006;
862
863        extern "C" {
864            fn proc_pidinfo(
865                pid: i32,
866                flavor: i32,
867                arg: u64,
868                buffer: *mut c_void,
869                buffersize: i32,
870            ) -> i32;
871            fn proc_pidpath(pid: i32, buffer: *mut c_void, buffersize: u32) -> i32;
872            fn getsockopt(fd: i32, level: i32, name: i32, val: *mut c_void, len: *mut u32) -> i32;
873        }
874
875        fn to_pid(pid: u32) -> io::Result<i32> {
876            i32::try_from(pid)
877                .ok()
878                .filter(|p| *p > 0)
879                .ok_or_else(|| io::Error::from(io::ErrorKind::InvalidInput))
880        }
881
882        /// The process's start time (seconds, microseconds) from `proc_pidinfo`. `Ok(None)` when
883        /// there is no such process.
884        pub fn start_marker(pid: u32) -> io::Result<Option<(u64, u64)>> {
885            let raw_pid = to_pid(pid)?;
886            let mut info = std::mem::MaybeUninit::<ProcBsdInfo>::zeroed();
887            let size = std::mem::size_of::<ProcBsdInfo>() as i32;
888            // SAFETY: `info` is a writable buffer of exactly `size` bytes; the call fills it.
889            let written = unsafe {
890                proc_pidinfo(
891                    raw_pid,
892                    PROC_PIDTBSDINFO,
893                    0,
894                    info.as_mut_ptr().cast::<c_void>(),
895                    size,
896                )
897            };
898            if written != size {
899                return missing_or_err();
900            }
901            // SAFETY: the kernel wrote the whole struct (`written == size`), and every field is
902            // a plain integer or byte array, so any bit pattern is valid.
903            let info = unsafe { info.assume_init() };
904            Ok((info.pbi_pid == pid).then_some((info.pbi_start_tvsec, info.pbi_start_tvusec)))
905        }
906
907        /// The process's executable path from `proc_pidpath`. `Ok(None)` when there is no such
908        /// process.
909        pub fn executable_of(pid: u32) -> io::Result<Option<ExeInfo>> {
910            let raw_pid = to_pid(pid)?;
911            let mut buf = vec![0u8; PROC_PIDPATHINFO_MAXSIZE];
912            // SAFETY: `buf` is a writable buffer of the length passed.
913            let n = unsafe {
914                proc_pidpath(raw_pid, buf.as_mut_ptr().cast::<c_void>(), buf.len() as u32)
915            };
916            if n <= 0 {
917                return missing_or_err();
918            }
919            buf.truncate(n as usize);
920            if let Some(nul) = buf.iter().position(|b| *b == 0) {
921                buf.truncate(nul);
922            }
923            let path = PathBuf::from(OsString::from_vec(buf));
924            let id = file_id(&path);
925            Ok(Some(ExeInfo { path, id }))
926        }
927
928        /// The peer's audit token (`LOCAL_PEERTOKEN`), when the kernel provides it.
929        pub fn audit_token(stream: &UnixStream) -> Option<[u32; 8]> {
930            let mut token = [0u32; 8];
931            let mut len = std::mem::size_of_val(&token) as u32;
932            // SAFETY: valid fd borrowed from `stream`; `token` and `len` describe a writable
933            // 32-byte `audit_token_t`.
934            let rc = unsafe {
935                getsockopt(
936                    stream.as_raw_fd(),
937                    SOL_LOCAL,
938                    LOCAL_PEERTOKEN,
939                    token.as_mut_ptr().cast::<c_void>(),
940                    &mut len,
941                )
942            };
943            (rc == 0 && len as usize == std::mem::size_of_val(&token)).then_some(token)
944        }
945    }
946
947    #[cfg(any(target_os = "linux", target_os = "android"))]
948    mod os {
949        use super::{is_no_such_process, ExeInfo};
950        use std::io;
951        use std::os::unix::fs::MetadataExt;
952        use std::os::unix::net::UnixStream;
953
954        pub const SUPPORTED: bool = true;
955
956        /// The process's start time in clock ticks since boot (field 22 of `/proc/<pid>/stat`).
957        /// `Ok(None)` when there is no such process.
958        pub fn start_marker(pid: u32) -> io::Result<Option<(u64, u64)>> {
959            let text = match std::fs::read_to_string(format!("/proc/{pid}/stat")) {
960                Ok(text) => text,
961                Err(e) if is_no_such_process(&e) => return Ok(None),
962                Err(e) => return Err(e),
963            };
964            let malformed = || io::Error::new(io::ErrorKind::InvalidData, "unreadable /proc stat");
965            // `pid (comm) state ...`: comm may hold spaces and parentheses, so split after the
966            // last ')'. The state is the first field after it, starttime the 20th.
967            let rest = text.rsplit_once(')').ok_or_else(malformed)?.1;
968            let ticks = rest
969                .split_whitespace()
970                .nth(19)
971                .and_then(|field| field.parse::<u64>().ok())
972                .ok_or_else(malformed)?;
973            Ok(Some((ticks, 0)))
974        }
975
976        /// The process's executable from `/proc/<pid>/exe`; its identity is the file the process
977        /// is really running (the magic link is followed, so a replaced or deleted binary is
978        /// still named exactly). `Ok(None)` when there is no such process.
979        pub fn executable_of(pid: u32) -> io::Result<Option<ExeInfo>> {
980            let link = format!("/proc/{pid}/exe");
981            let path = match std::fs::read_link(&link) {
982                Ok(path) => path,
983                Err(e) if is_no_such_process(&e) => return Ok(None),
984                Err(e) => return Err(e),
985            };
986            let id = std::fs::metadata(&link).ok().map(|md| (md.dev(), md.ino()));
987            Ok(Some(ExeInfo { path, id }))
988        }
989
990        /// Linux has no audit token; the start time is the witness.
991        pub fn audit_token(_stream: &UnixStream) -> Option<[u32; 8]> {
992            None
993        }
994    }
995
996    /// Any other Unix: the executable of a process cannot be read here, so no host is verified.
997    #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "android")))]
998    mod os {
999        use super::ExeInfo;
1000        use std::io;
1001        use std::os::unix::net::UnixStream;
1002
1003        pub const SUPPORTED: bool = false;
1004
1005        pub fn start_marker(_pid: u32) -> io::Result<Option<(u64, u64)>> {
1006            Err(io::Error::from(io::ErrorKind::Unsupported))
1007        }
1008        pub fn executable_of(_pid: u32) -> io::Result<Option<ExeInfo>> {
1009            Err(io::Error::from(io::ErrorKind::Unsupported))
1010        }
1011        pub fn audit_token(_stream: &UnixStream) -> Option<[u32; 8]> {
1012            None
1013        }
1014    }
1015
1016    /// Linux: is this the error of a process that does not exist (`ENOENT` on the `/proc` entry,
1017    /// or `ESRCH` when it died mid-read)?
1018    #[cfg(any(target_os = "linux", target_os = "android"))]
1019    fn is_no_such_process(e: &io::Error) -> bool {
1020        e.kind() == io::ErrorKind::NotFound || e.raw_os_error() == Some(3)
1021    }
1022}
1023
1024#[cfg(windows)]
1025pub(crate) use windows_impl::{verify_pid_host, verify_pipe_host};
1026
1027/// Windows: which program holds the server end of a named pipe this client opened.
1028#[cfg(windows)]
1029mod windows_impl {
1030    use super::*;
1031    use crate::peer::{verify_pipe_server_is_current_user, PeerError, PeerIdentity};
1032    use std::ffi::OsString;
1033    use std::os::windows::ffi::OsStringExt;
1034    use std::os::windows::io::{AsRawHandle, RawHandle};
1035
1036    const PROCESS_QUERY_LIMITED_INFORMATION: u32 = 0x1000;
1037    const SYNCHRONIZE: u32 = 0x0010_0000;
1038    const WAIT_TIMEOUT: u32 = 258;
1039    const ERROR_INVALID_PARAMETER: i32 = 87;
1040    /// The longest path `QueryFullProcessImageNameW` can return, in UTF-16 units.
1041    const LONGEST_PATH: usize = 32_768;
1042
1043    #[repr(C)]
1044    #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1045    #[allow(dead_code)]
1046    struct FileTime {
1047        low: u32,
1048        high: u32,
1049    }
1050
1051    /// `BY_HANDLE_FILE_INFORMATION`.
1052    #[repr(C)]
1053    #[derive(Default)]
1054    #[allow(dead_code)]
1055    struct ByHandleFileInformation {
1056        attributes: u32,
1057        creation: FileTime,
1058        access: FileTime,
1059        write: FileTime,
1060        volume_serial: u32,
1061        size_high: u32,
1062        size_low: u32,
1063        links: u32,
1064        index_high: u32,
1065        index_low: u32,
1066    }
1067
1068    #[link(name = "kernel32")]
1069    unsafe extern "system" {
1070        fn GetNamedPipeServerProcessId(pipe: RawHandle, pid: *mut u32) -> i32;
1071        fn OpenProcess(access: u32, inherit: i32, pid: u32) -> RawHandle;
1072        fn GetProcessTimes(
1073            process: RawHandle,
1074            creation: *mut FileTime,
1075            exit: *mut FileTime,
1076            kernel: *mut FileTime,
1077            user: *mut FileTime,
1078        ) -> i32;
1079        fn QueryFullProcessImageNameW(
1080            process: RawHandle,
1081            flags: u32,
1082            name: *mut u16,
1083            size: *mut u32,
1084        ) -> i32;
1085        fn WaitForSingleObject(handle: RawHandle, milliseconds: u32) -> u32;
1086        fn CloseHandle(handle: RawHandle) -> i32;
1087        fn GetFileInformationByHandle(file: RawHandle, info: *mut ByHandleFileInformation) -> i32;
1088    }
1089
1090    /// A process handle. While it is open the process object stays, so its pid is not reused.
1091    struct Process(RawHandle);
1092
1093    impl Drop for Process {
1094        fn drop(&mut self) {
1095            // SAFETY: the handle came from OpenProcess and is closed once.
1096            unsafe { CloseHandle(self.0) };
1097        }
1098    }
1099
1100    impl Process {
1101        fn open(pid: u32) -> io::Result<Process> {
1102            // SAFETY: plain call; a null result is the failure.
1103            let handle =
1104                unsafe { OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION | SYNCHRONIZE, 0, pid) };
1105            if handle.is_null() {
1106                return Err(io::Error::last_os_error());
1107            }
1108            Ok(Process(handle))
1109        }
1110
1111        /// When the process was created: with the pid, the identity of one process.
1112        fn created(&self) -> io::Result<FileTime> {
1113            let mut times = [FileTime::default(); 4];
1114            let [creation, exit, kernel, user] = &mut times;
1115            // SAFETY: four valid out-parameters for an open process handle.
1116            if unsafe { GetProcessTimes(self.0, creation, exit, kernel, user) } == 0 {
1117                return Err(io::Error::last_os_error());
1118            }
1119            Ok(times[0])
1120        }
1121
1122        fn running(&self) -> bool {
1123            // SAFETY: the handle was opened with SYNCHRONIZE.
1124            unsafe { WaitForSingleObject(self.0, 0) == WAIT_TIMEOUT }
1125        }
1126
1127        /// The Win32 path of the image the process runs.
1128        fn image(&self) -> io::Result<PathBuf> {
1129            let mut buffer = vec![0u16; LONGEST_PATH];
1130            let mut length = buffer.len() as u32;
1131            // SAFETY: `buffer` holds `length` units; the call writes at most that many and
1132            // sets `length` to the number written, without the terminator.
1133            if unsafe { QueryFullProcessImageNameW(self.0, 0, buffer.as_mut_ptr(), &mut length) }
1134                == 0
1135            {
1136                return Err(io::Error::last_os_error());
1137            }
1138            Ok(PathBuf::from(OsString::from_wide(
1139                &buffer[..length as usize],
1140            )))
1141        }
1142    }
1143
1144    fn server_pid(pipe: RawHandle) -> io::Result<u32> {
1145        let mut pid = 0u32;
1146        // SAFETY: `pipe` is the client end of a named pipe; `pid` is a valid out-parameter.
1147        if unsafe { GetNamedPipeServerProcessId(pipe, &mut pid) } == 0 {
1148            return Err(io::Error::last_os_error());
1149        }
1150        Ok(pid)
1151    }
1152
1153    /// Volume serial number and file index: the same file under any name, as device and inode
1154    /// are on Unix.
1155    pub(super) fn file_id(path: &Path) -> Option<(u32, u64)> {
1156        let file = std::fs::File::open(path).ok()?;
1157        let mut info = ByHandleFileInformation::default();
1158        // SAFETY: an open file handle and a valid out-parameter of the documented layout.
1159        if unsafe { GetFileInformationByHandle(file.as_raw_handle(), &mut info) } == 0 {
1160            return None;
1161        }
1162        Some((
1163            info.volume_serial,
1164            (u64::from(info.index_high) << 32) | u64::from(info.index_low),
1165        ))
1166    }
1167
1168    /// Whether `expected` names the file at `found` (already canonical): the same canonical path
1169    /// without regard to case (NTFS keeps case and ignores it), or the same file by identity (a
1170    /// hard link).
1171    pub(super) fn same_executable(
1172        expected: &Path,
1173        found: &Path,
1174        found_id: Option<(u32, u64)>,
1175    ) -> bool {
1176        let Ok(expected) = std::fs::canonicalize(expected) else {
1177            return false;
1178        };
1179        let lower = |path: &Path| path.to_string_lossy().to_lowercase();
1180        if lower(&expected) == lower(found) {
1181            return true;
1182        }
1183        found_id.is_some() && file_id(&expected) == found_id
1184    }
1185
1186    /// A canonical path as a person writes it: without the `\\?\` prefix.
1187    fn plain(path: PathBuf) -> PathBuf {
1188        let text = path.to_string_lossy();
1189        match text.strip_prefix(r"\\?\UNC\") {
1190            Some(rest) => PathBuf::from(format!(r"\\{rest}")),
1191            None => match text.strip_prefix(r"\\?\") {
1192                Some(rest) => PathBuf::from(rest),
1193                None => path,
1194            },
1195        }
1196    }
1197
1198    /// Identify the process that holds the server end of `pipe` (the client end, opened with
1199    /// `SECURITY_SQOS_PRESENT | SECURITY_IDENTIFICATION`) and check it against `expect`, before
1200    /// any byte is written:
1201    ///
1202    /// 1. the pipe's server pid, and that process's token user is this client's (the existing
1203    ///    same-user check, repeated here for its identity);
1204    /// 2. a handle to that process, kept until the end: it pins the process object, so the pid
1205    ///    cannot be given to another process while it is held;
1206    /// 3. the process's creation time, its image path, then the creation time again, that it is
1207    ///    still running, and that the pipe still reports the same pid;
1208    /// 4. the image path, canonicalised, is one of the expected executables (same canonical
1209    ///    path ignoring case, or the same file by volume and index);
1210    /// 5. the caller's extra verifier, if any. A code-signature check (`WinVerifyTrust` on
1211    ///    [`HostProcess::executable`]) belongs there.
1212    pub(crate) fn verify_pipe_host(
1213        pipe: RawHandle,
1214        expect: &HostExpectation,
1215    ) -> Result<HostProcess, HostConnectError> {
1216        let refuse = |pid: Option<u32>, exe: Option<PathBuf>, reason: HostIdentityReason| {
1217            HostConnectError::Identity(HostIdentityError {
1218                expected: expect.executables.clone(),
1219                found_pid: pid,
1220                found_executable: exe,
1221                reason,
1222            })
1223        };
1224        if expect.executables.is_empty() {
1225            return Err(refuse(None, None, HostIdentityReason::NoExpectation));
1226        }
1227
1228        let first = match verify_pipe_server_is_current_user(pipe) {
1229            Ok(id) => id,
1230            Err(e) => {
1231                // The host hung up before it could be identified: a connection problem.
1232                if e.peer_gone() {
1233                    return Err(HostConnectError::Io(io::Error::from(e)));
1234                }
1235                let pid = match &e {
1236                    PeerError::Mismatch(id) => id.pid,
1237                    _ => None,
1238                };
1239                return Err(refuse(pid, None, HostIdentityReason::Query(e.to_string())));
1240            }
1241        };
1242        let Some(pid) = first.pid else {
1243            return Err(refuse(None, None, HostIdentityReason::PidUnavailable));
1244        };
1245        let unreadable = |what: &str, e: io::Error| {
1246            refuse(
1247                Some(pid),
1248                None,
1249                HostIdentityReason::Unreadable(format!("{what}: {e}")),
1250            )
1251        };
1252        let changed = |why: &str| {
1253            refuse(
1254                Some(pid),
1255                None,
1256                HostIdentityReason::HostChanged(why.to_string()),
1257            )
1258        };
1259
1260        let process = match Process::open(pid) {
1261            Ok(process) => process,
1262            // No process has this id any more.
1263            Err(e) if e.raw_os_error() == Some(ERROR_INVALID_PARAMETER) => {
1264                return Err(changed("it exited before it could be opened"))
1265            }
1266            Err(e) => return Err(unreadable("open process", e)),
1267        };
1268        let before = process
1269            .created()
1270            .map_err(|e| unreadable("creation time", e))?;
1271        let image = process
1272            .image()
1273            .map_err(|e| unreadable("executable path", e))?;
1274        let after = process
1275            .created()
1276            .map_err(|e| unreadable("creation time", e))?;
1277        if before != after {
1278            return Err(changed(
1279                "its creation time differed before and after its executable path was read",
1280            ));
1281        }
1282        if !process.running() {
1283            return Err(changed("it exited while it was being identified"));
1284        }
1285        match server_pid(pipe) {
1286            Ok(again) if again == pid => {}
1287            _ => return Err(changed("the pipe no longer reports the same process id")),
1288        }
1289
1290        let found = std::fs::canonicalize(&image).unwrap_or(image);
1291        let id = file_id(&found);
1292        if !expect
1293            .executables
1294            .iter()
1295            .any(|expected| same_executable(expected, &found, id))
1296        {
1297            return Err(refuse(
1298                Some(pid),
1299                Some(plain(found)),
1300                HostIdentityReason::ExecutableMismatch,
1301            ));
1302        }
1303
1304        let host = HostProcess {
1305            pid,
1306            uid: None,
1307            sid: first.sid,
1308            executable: plain(found),
1309            audit_token: None,
1310        };
1311        if let Some(verifier) = &expect.verifier {
1312            if let Err(why) = verifier(&host) {
1313                return Err(refuse(
1314                    Some(pid),
1315                    Some(host.executable.clone()),
1316                    HostIdentityReason::Refused(why),
1317                ));
1318            }
1319        }
1320        // The process handle is released only now: every check above saw one process.
1321        drop(process);
1322        Ok(host)
1323    }
1324
1325    pub(super) fn start_marker_of(pid: u32) -> Option<(u64, u64)> {
1326        let t = Process::open(pid).ok()?.created().ok()?;
1327        Some(((u64::from(t.high) << 32) | u64::from(t.low), 0))
1328    }
1329
1330    /// Server side: identify the process that opened a named-pipe connection, from the
1331    /// OS-verified [`PeerIdentity`] recorded when it connected, and decide whether it is the
1332    /// program `expect` names. The same executable comparison and verifier as
1333    /// [`verify_pipe_host`], with the pipe handle no longer at hand: a handle to the process is
1334    /// opened and kept until every check has run (it pins the process object, so its pid cannot
1335    /// name another process meanwhile), and the creation time is read before and after the image
1336    /// path.
1337    ///
1338    /// Fails closed: no pid or no user SID recorded for the peer, no expected executable, a
1339    /// process that cannot be opened or has exited, a creation time that changed, an unreadable
1340    /// image, an executable that is not expected, a refusing verifier. An Authenticode check
1341    /// (`WinVerifyTrust` on [`HostProcess::executable`]) belongs in the verifier.
1342    pub(crate) fn verify_pid_host(
1343        peer: &PeerIdentity,
1344        expect: &HostExpectation,
1345    ) -> Result<HostProcess, HostIdentityError> {
1346        let refuse = |pid: Option<u32>, exe: Option<PathBuf>, reason: HostIdentityReason| {
1347            HostIdentityError {
1348                expected: expect.executables.clone(),
1349                found_pid: pid,
1350                found_executable: exe,
1351                reason,
1352            }
1353        };
1354        if expect.executables.is_empty() && !expect.any_executable {
1355            return Err(refuse(None, None, HostIdentityReason::NoExpectation));
1356        }
1357        let Some(pid) = peer.pid else {
1358            return Err(refuse(None, None, HostIdentityReason::PidUnavailable));
1359        };
1360        let Some(sid) = peer.sid.clone() else {
1361            return Err(refuse(
1362                Some(pid),
1363                None,
1364                HostIdentityReason::Query("no user SID was recorded for the peer".to_string()),
1365            ));
1366        };
1367        let unreadable = |what: &str, e: io::Error| {
1368            refuse(
1369                Some(pid),
1370                None,
1371                HostIdentityReason::Unreadable(format!("{what}: {e}")),
1372            )
1373        };
1374        let changed = |why: &str| {
1375            refuse(
1376                Some(pid),
1377                None,
1378                HostIdentityReason::HostChanged(why.to_string()),
1379            )
1380        };
1381        let process = match Process::open(pid) {
1382            Ok(process) => process,
1383            Err(e) if e.raw_os_error() == Some(ERROR_INVALID_PARAMETER) => {
1384                return Err(changed("it exited before it could be opened"))
1385            }
1386            Err(e) => return Err(unreadable("open process", e)),
1387        };
1388        let before = process
1389            .created()
1390            .map_err(|e| unreadable("creation time", e))?;
1391        // The pid must still be the process that connected (creation time recorded at accept).
1392        let marker = ((u64::from(before.high) << 32) | u64::from(before.low), 0);
1393        if peer.started != Some(marker) {
1394            return Err(changed(concat!(
1395                "the process is not the one that connected (its creation time differs ",
1396                "from the one recorded at connect, or none was recorded)"
1397            )));
1398        }
1399        let image = process
1400            .image()
1401            .map_err(|e| unreadable("executable path", e))?;
1402        let after = process
1403            .created()
1404            .map_err(|e| unreadable("creation time", e))?;
1405        if before != after {
1406            return Err(changed(
1407                "its creation time differed before and after its executable path was read",
1408            ));
1409        }
1410        if !process.running() {
1411            return Err(changed("it exited while it was being identified"));
1412        }
1413
1414        let found = std::fs::canonicalize(&image).unwrap_or(image);
1415        let id = file_id(&found);
1416        if !expect.any_executable
1417            && !expect
1418                .executables
1419                .iter()
1420                .any(|expected| same_executable(expected, &found, id))
1421        {
1422            return Err(refuse(
1423                Some(pid),
1424                Some(plain(found)),
1425                HostIdentityReason::ExecutableMismatch,
1426            ));
1427        }
1428        let host = HostProcess {
1429            pid,
1430            uid: None,
1431            sid: Some(sid),
1432            executable: plain(found),
1433            audit_token: None,
1434        };
1435        if let Some(verifier) = &expect.verifier {
1436            if let Err(why) = verifier(&host) {
1437                return Err(refuse(
1438                    Some(pid),
1439                    Some(host.executable.clone()),
1440                    HostIdentityReason::Refused(why),
1441                ));
1442            }
1443        }
1444        // Released only now: every check above saw one process.
1445        drop(process);
1446        Ok(host)
1447    }
1448}