1use std::ffi::OsStr;
16use std::fmt;
17use std::io;
18use std::path::{Path, PathBuf};
19use std::sync::Arc;
20
21#[derive(Debug, Clone, PartialEq, Eq)]
26#[non_exhaustive]
27pub struct HostProcess {
28 pub pid: u32,
31 pub uid: Option<u32>,
33 pub sid: Option<String>,
35 pub executable: PathBuf,
38 pub audit_token: Option<[u32; 8]>,
44}
45
46#[derive(Debug, Clone, PartialEq, Eq)]
48#[non_exhaustive]
49pub enum HostIdentityReason {
50 NoExpectation,
52 UidMismatch { host: Option<u32>, client: u32 },
54 PidUnavailable,
56 Unreadable(String),
58 ExecutableMismatch,
60 HostChanged(String),
63 Refused(String),
65 Unverified {
68 platform: &'static str,
69 detail: &'static str,
70 },
71 Query(String),
73 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#[derive(Debug, Clone, PartialEq, Eq)]
144#[non_exhaustive]
145pub struct HostIdentityError {
146 pub expected: Vec<PathBuf>,
148 pub found_pid: Option<u32>,
150 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#[derive(Debug)]
187pub(crate) enum HostConnectError {
188 Io(io::Error),
190 Identity(HostIdentityError),
192}
193
194type Verifier = Arc<dyn Fn(&HostProcess) -> Result<(), String> + Send + Sync>;
195
196#[derive(Clone, Default)]
214pub struct HostExpectation {
215 executables: Vec<PathBuf>,
216 verifier: Option<Verifier>,
217}
218
219impl fmt::Debug for HostExpectation {
220 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
221 f.debug_struct("HostExpectation")
222 .field("executables", &self.executables)
223 .field("verifier", &self.verifier.as_ref().map(|_| "<fn>"))
224 .finish()
225 }
226}
227
228fn invalid(what: &str) -> io::Error {
229 io::Error::new(io::ErrorKind::InvalidInput, what.to_string())
230}
231
232fn current_exe_canonical() -> io::Result<PathBuf> {
233 std::fs::canonicalize(std::env::current_exe()?)
234}
235
236fn is_single_component(name: &OsStr) -> bool {
238 let path = Path::new(name);
239 path.file_name() == Some(name) && path.components().count() == 1
240}
241
242fn up_and_join(exe: &Path, levels_up: usize, relative: &Path) -> Option<PathBuf> {
247 use std::path::Component;
248 let leaves_base = relative
249 .components()
250 .any(|part| matches!(part, Component::Prefix(_) | Component::RootDir));
251 if levels_up == 0 || relative.is_absolute() || leaves_base {
252 return None;
253 }
254 exe.ancestors()
255 .nth(levels_up)
256 .map(|base| base.join(relative))
257}
258
259fn bundle_main(exe: &Path, file_name: &OsStr) -> Option<PathBuf> {
261 exe.ancestors()
262 .skip(1)
263 .find(|dir| dir.extension() == Some(OsStr::new("app")))
264 .map(|bundle| bundle.join("Contents").join("MacOS").join(file_name))
265}
266
267impl HostExpectation {
268 pub fn executable(path: impl Into<PathBuf>) -> Self {
270 Self {
271 executables: vec![path.into()],
272 verifier: None,
273 }
274 }
275
276 pub fn any_of<I, P>(paths: I) -> Self
278 where
279 I: IntoIterator<Item = P>,
280 P: Into<PathBuf>,
281 {
282 Self {
283 executables: paths.into_iter().map(Into::into).collect(),
284 verifier: None,
285 }
286 }
287
288 pub fn or_executable(mut self, path: impl Into<PathBuf>) -> Self {
290 self.executables.push(path.into());
291 self
292 }
293
294 pub fn sibling_executable(file_name: impl AsRef<OsStr>) -> io::Result<Self> {
298 let name = file_name.as_ref();
299 if !is_single_component(name) {
300 return Err(invalid("the host file name must be a bare file name"));
301 }
302 Self::relative_to_current_exe(1, name)
303 }
304
305 pub fn bundle_executable(file_name: impl AsRef<OsStr>) -> io::Result<Self> {
311 let name = file_name.as_ref();
312 if !is_single_component(name) {
313 return Err(invalid("the host file name must be a bare file name"));
314 }
315 let exe = current_exe_canonical()?;
316 let path = bundle_main(&exe, name).ok_or_else(|| {
317 io::Error::new(
318 io::ErrorKind::NotFound,
319 "the running executable is not inside an .app bundle",
320 )
321 })?;
322 Ok(Self::executable(path))
323 }
324
325 pub fn relative_to_current_exe(
330 levels_up: usize,
331 relative: impl AsRef<Path>,
332 ) -> io::Result<Self> {
333 let exe = current_exe_canonical()?;
334 let path = up_and_join(&exe, levels_up, relative.as_ref()).ok_or_else(|| {
335 invalid("no such directory above the running executable, or the path is absolute")
336 })?;
337 Ok(Self::executable(path))
338 }
339
340 pub fn with_verifier(
352 mut self,
353 check: impl Fn(&HostProcess) -> Result<(), String> + Send + Sync + 'static,
354 ) -> Self {
355 self.verifier = Some(Arc::new(check));
356 self
357 }
358
359 pub fn executables(&self) -> &[PathBuf] {
361 &self.executables
362 }
363}
364
365pub(crate) fn verify_peer_process(
369 peer: Option<&crate::peer::PeerIdentity>,
370 expect: &HostExpectation,
371) -> Result<HostProcess, HostIdentityError> {
372 let Some(peer) = peer else {
373 return Err(HostIdentityError {
374 expected: expect.executables.clone(),
375 found_pid: None,
376 found_executable: None,
377 reason: HostIdentityReason::NotLocalPeer,
378 });
379 };
380 #[cfg(unix)]
381 {
382 verify_unix_pid(peer, expect)
383 }
384 #[cfg(windows)]
385 {
386 verify_pid_host(peer, expect)
387 }
388 #[cfg(not(any(unix, windows)))]
389 {
390 Err(HostIdentityError {
391 expected: expect.executables.clone(),
392 found_pid: peer.pid,
393 found_executable: None,
394 reason: HostIdentityReason::Unverified {
395 platform: std::env::consts::OS,
396 detail: "no process identification is implemented for this platform",
397 },
398 })
399 }
400}
401
402pub(crate) fn process_start_marker(pid: u32) -> Option<(u64, u64)> {
405 #[cfg(unix)]
406 {
407 unix_impl::start_marker_of(pid)
408 }
409 #[cfg(windows)]
410 {
411 windows_impl::start_marker_of(pid)
412 }
413 #[cfg(not(any(unix, windows)))]
414 {
415 let _ = pid;
416 None
417 }
418}
419
420#[cfg(unix)]
421pub(crate) use unix_impl::{verify_unix_host, verify_unix_pid};
422#[cfg(all(
424 unix,
425 any(
426 target_os = "macos",
427 target_os = "ios",
428 target_os = "freebsd",
429 target_os = "openbsd",
430 target_os = "netbsd",
431 target_os = "dragonfly"
432 )
433))]
434pub(crate) use unix_impl::peer_audit_token;
435
436#[cfg(unix)]
437mod unix_impl {
438 use super::{
439 HostConnectError, HostExpectation, HostIdentityError, HostIdentityReason, HostProcess,
440 };
441 use crate::peer::{current_euid, verify_peer_is_current_user, PeerError, PeerIdentity};
442 use std::io;
443 use std::os::unix::fs::MetadataExt;
444 use std::os::unix::net::UnixStream;
445 use std::path::{Path, PathBuf};
446
447 pub type FileId = (u64, u64);
449
450 pub fn file_id(path: &Path) -> Option<FileId> {
451 let md = std::fs::metadata(path).ok()?;
452 Some((md.dev(), md.ino()))
453 }
454
455 pub struct ExeInfo {
457 pub path: PathBuf,
458 pub id: Option<FileId>,
461 }
462
463 #[derive(PartialEq, Eq)]
466 struct Witness {
467 start: (u64, u64),
468 token: Option<[u32; 8]>,
469 }
470
471 enum ObserveError {
472 Gone,
473 Unreadable(String),
474 }
475
476 impl From<ObserveError> for HostIdentityReason {
477 fn from(e: ObserveError) -> Self {
478 match e {
479 ObserveError::Gone => HostIdentityReason::HostChanged(
480 "it exited before it could be identified".to_string(),
481 ),
482 ObserveError::Unreadable(why) => HostIdentityReason::Unreadable(why),
483 }
484 }
485 }
486
487 #[cfg(target_os = "macos")]
490 fn missing_or_err<T>() -> io::Result<Option<T>> {
491 let e = io::Error::last_os_error();
492 match e.raw_os_error() {
493 Some(3) | Some(0) | None => Ok(None),
494 _ => Err(e),
495 }
496 }
497
498 fn witness(stream: &UnixStream, pid: u32) -> Result<Witness, ObserveError> {
499 let token = os::audit_token(stream);
500 let start = match os::start_marker(pid) {
501 Ok(Some(start)) => start,
502 Ok(None) => return Err(ObserveError::Gone),
503 Err(e) => return Err(ObserveError::Unreadable(format!("process start time: {e}"))),
504 };
505 Ok(Witness { start, token })
506 }
507
508 pub fn same_executable(expected: &Path, found: &Path, found_id: Option<FileId>) -> bool {
514 let Ok(canonical) = std::fs::canonicalize(expected) else {
515 return false;
516 };
517 if canonical == found {
518 return true;
519 }
520 match (found_id, file_id(&canonical)) {
521 (Some(running), Some(named)) => running == named,
522 _ => false,
523 }
524 }
525
526 pub(crate) fn verify_unix_host(
538 stream: &UnixStream,
539 expect: &HostExpectation,
540 ) -> Result<HostProcess, HostConnectError> {
541 let refuse = |pid: Option<u32>, exe: Option<PathBuf>, reason: HostIdentityReason| {
542 HostConnectError::Identity(HostIdentityError {
543 expected: expect.executables.clone(),
544 found_pid: pid,
545 found_executable: exe,
546 reason,
547 })
548 };
549 if expect.executables.is_empty() {
550 return Err(refuse(None, None, HostIdentityReason::NoExpectation));
551 }
552 if !os::SUPPORTED {
553 return Err(refuse(
554 None,
555 None,
556 HostIdentityReason::Unverified {
557 platform: std::env::consts::OS,
558 detail: "reading a process's executable path is not implemented for this Unix",
559 },
560 ));
561 }
562
563 let first = match verify_peer_is_current_user(stream) {
564 Ok(id) => id,
565 Err(PeerError::Mismatch(id)) => {
566 return Err(refuse(
567 id.pid,
568 None,
569 HostIdentityReason::UidMismatch {
570 host: id.uid,
571 client: current_euid(),
572 },
573 ))
574 }
575 Err(PeerError::Unsupported(detail)) => {
576 return Err(refuse(
577 None,
578 None,
579 HostIdentityReason::Unverified {
580 platform: std::env::consts::OS,
581 detail,
582 },
583 ))
584 }
585 Err(e) => {
586 if e.peer_gone() {
588 return Err(HostConnectError::Io(io::Error::from(e)));
589 }
590 return Err(refuse(None, None, HostIdentityReason::Query(e.to_string())));
591 }
592 };
593 let Some(pid) = first.pid else {
594 return Err(refuse(None, None, HostIdentityReason::PidUnavailable));
595 };
596
597 let observe = |e: ObserveError| refuse(Some(pid), None, e.into());
598 let changed = |why: &str| {
599 refuse(
600 Some(pid),
601 None,
602 HostIdentityReason::HostChanged(why.to_string()),
603 )
604 };
605
606 let before = witness(stream, pid).map_err(&observe)?;
607 let exe = match os::executable_of(pid) {
608 Ok(Some(exe)) => exe,
609 Ok(None) => return Err(observe(ObserveError::Gone)),
610 Err(e) => {
611 return Err(observe(ObserveError::Unreadable(format!(
612 "executable path: {e}"
613 ))))
614 }
615 };
616 let after = witness(stream, pid).map_err(&observe)?;
617 if before != after {
618 return Err(changed(
619 "its identity differed before and after its executable path was read",
620 ));
621 }
622 match verify_peer_is_current_user(stream) {
623 Ok(again) if again.pid == Some(pid) => {}
624 _ => return Err(changed("the socket no longer reports the same process id")),
625 }
626
627 conclude(expect, pid, exe, after.token).map_err(HostConnectError::Identity)
628 }
629
630 fn conclude(
634 expect: &HostExpectation,
635 pid: u32,
636 exe: ExeInfo,
637 audit_token: Option<[u32; 8]>,
638 ) -> Result<HostProcess, HostIdentityError> {
639 let refuse = |exe: Option<PathBuf>, reason: HostIdentityReason| HostIdentityError {
640 expected: expect.executables.clone(),
641 found_pid: Some(pid),
642 found_executable: exe,
643 reason,
644 };
645 let ExeInfo { path, id } = exe;
646 let found = std::fs::canonicalize(&path).unwrap_or(path);
647 if !expect
648 .executables
649 .iter()
650 .any(|expected| same_executable(expected, &found, id))
651 {
652 return Err(refuse(Some(found), HostIdentityReason::ExecutableMismatch));
653 }
654
655 let host = HostProcess {
656 pid,
657 uid: Some(current_euid()),
658 sid: None,
659 executable: found,
660 audit_token,
661 };
662 if let Some(verifier) = &expect.verifier {
663 if let Err(why) = verifier(&host) {
664 return Err(refuse(
665 Some(host.executable.clone()),
666 HostIdentityReason::Refused(why),
667 ));
668 }
669 }
670 Ok(host)
671 }
672
673 pub(crate) fn verify_unix_pid(
684 peer: &PeerIdentity,
685 expect: &HostExpectation,
686 ) -> Result<HostProcess, HostIdentityError> {
687 let refuse = |pid: Option<u32>, exe: Option<PathBuf>, reason: HostIdentityReason| {
688 HostIdentityError {
689 expected: expect.executables.clone(),
690 found_pid: pid,
691 found_executable: exe,
692 reason,
693 }
694 };
695 if expect.executables.is_empty() {
696 return Err(refuse(None, None, HostIdentityReason::NoExpectation));
697 }
698 if !os::SUPPORTED {
699 return Err(refuse(
700 None,
701 None,
702 HostIdentityReason::Unverified {
703 platform: std::env::consts::OS,
704 detail: "reading a process's executable path is not implemented for this Unix",
705 },
706 ));
707 }
708 if peer.uid != Some(current_euid()) {
709 return Err(refuse(
710 peer.pid,
711 None,
712 HostIdentityReason::UidMismatch {
713 host: peer.uid,
714 client: current_euid(),
715 },
716 ));
717 }
718 let Some(pid) = peer.pid else {
719 return Err(refuse(None, None, HostIdentityReason::PidUnavailable));
720 };
721 if let Some(token) = peer.audit_token {
722 if token[5] != pid {
724 return Err(refuse(
725 Some(pid),
726 None,
727 HostIdentityReason::HostChanged(
728 "the audit token and the process id name different processes".to_string(),
729 ),
730 ));
731 }
732 }
733 let observe = |e: ObserveError| refuse(Some(pid), None, e.into());
734 let start = |pid| match os::start_marker(pid) {
735 Ok(Some(start)) => Ok(start),
736 Ok(None) => Err(ObserveError::Gone),
737 Err(e) => Err(ObserveError::Unreadable(format!("process start time: {e}"))),
738 };
739 let before = start(pid).map_err(&observe)?;
740 if peer.started != Some(before) {
744 return Err(refuse(
745 Some(pid),
746 None,
747 HostIdentityReason::HostChanged(
748 concat!(
749 "the process is not the one that connected (its start time differs ",
750 "from the one recorded at connect, or none was recorded)"
751 )
752 .to_string(),
753 ),
754 ));
755 }
756 let exe = match os::executable_of(pid) {
757 Ok(Some(exe)) => exe,
758 Ok(None) => return Err(observe(ObserveError::Gone)),
759 Err(e) => {
760 return Err(observe(ObserveError::Unreadable(format!(
761 "executable path: {e}"
762 ))))
763 }
764 };
765 let after = start(pid).map_err(&observe)?;
766 if before != after {
767 return Err(refuse(
768 Some(pid),
769 None,
770 HostIdentityReason::HostChanged(
771 "its start time differed before and after its executable path was read"
772 .to_string(),
773 ),
774 ));
775 }
776 conclude(expect, pid, exe, peer.audit_token)
777 }
778
779 pub(crate) fn start_marker_of(pid: u32) -> Option<(u64, u64)> {
780 os::start_marker(pid).ok().flatten()
781 }
782
783 #[cfg(any(
785 target_os = "macos",
786 target_os = "ios",
787 target_os = "freebsd",
788 target_os = "openbsd",
789 target_os = "netbsd",
790 target_os = "dragonfly"
791 ))]
792 pub(crate) fn peer_audit_token(stream: &UnixStream) -> Option<[u32; 8]> {
793 os::audit_token(stream)
794 }
795
796 #[cfg(target_os = "macos")]
797 mod os {
798 use super::{file_id, missing_or_err, ExeInfo};
799 use std::ffi::{c_void, OsString};
800 use std::io;
801 use std::os::fd::AsRawFd;
802 use std::os::unix::ffi::OsStringExt;
803 use std::os::unix::net::UnixStream;
804 use std::path::PathBuf;
805
806 pub const SUPPORTED: bool = true;
807
808 #[repr(C)]
811 #[allow(dead_code)]
812 struct ProcBsdInfo {
813 pbi_flags: u32,
814 pbi_status: u32,
815 pbi_xstatus: u32,
816 pbi_pid: u32,
817 pbi_ppid: u32,
818 pbi_uid: u32,
819 pbi_gid: u32,
820 pbi_ruid: u32,
821 pbi_rgid: u32,
822 pbi_svuid: u32,
823 pbi_svgid: u32,
824 rfu_1: u32,
825 pbi_comm: [u8; 16],
826 pbi_name: [u8; 32],
827 pbi_nfiles: u32,
828 pbi_pgid: u32,
829 pbi_pjobc: u32,
830 e_tdev: u32,
831 e_tpgid: u32,
832 pbi_nice: i32,
833 pbi_start_tvsec: u64,
834 pbi_start_tvusec: u64,
835 }
836 const _: () = assert!(std::mem::size_of::<ProcBsdInfo>() == 136);
837
838 const PROC_PIDTBSDINFO: i32 = 3;
839 const PROC_PIDPATHINFO_MAXSIZE: usize = 4 * 1024;
840 const SOL_LOCAL: i32 = 0;
841 const LOCAL_PEERTOKEN: i32 = 0x006;
842
843 extern "C" {
844 fn proc_pidinfo(
845 pid: i32,
846 flavor: i32,
847 arg: u64,
848 buffer: *mut c_void,
849 buffersize: i32,
850 ) -> i32;
851 fn proc_pidpath(pid: i32, buffer: *mut c_void, buffersize: u32) -> i32;
852 fn getsockopt(fd: i32, level: i32, name: i32, val: *mut c_void, len: *mut u32) -> i32;
853 }
854
855 fn to_pid(pid: u32) -> io::Result<i32> {
856 i32::try_from(pid)
857 .ok()
858 .filter(|p| *p > 0)
859 .ok_or_else(|| io::Error::from(io::ErrorKind::InvalidInput))
860 }
861
862 pub fn start_marker(pid: u32) -> io::Result<Option<(u64, u64)>> {
865 let raw_pid = to_pid(pid)?;
866 let mut info = std::mem::MaybeUninit::<ProcBsdInfo>::zeroed();
867 let size = std::mem::size_of::<ProcBsdInfo>() as i32;
868 let written = unsafe {
870 proc_pidinfo(
871 raw_pid,
872 PROC_PIDTBSDINFO,
873 0,
874 info.as_mut_ptr().cast::<c_void>(),
875 size,
876 )
877 };
878 if written != size {
879 return missing_or_err();
880 }
881 let info = unsafe { info.assume_init() };
884 Ok((info.pbi_pid == pid).then_some((info.pbi_start_tvsec, info.pbi_start_tvusec)))
885 }
886
887 pub fn executable_of(pid: u32) -> io::Result<Option<ExeInfo>> {
890 let raw_pid = to_pid(pid)?;
891 let mut buf = vec![0u8; PROC_PIDPATHINFO_MAXSIZE];
892 let n = unsafe {
894 proc_pidpath(raw_pid, buf.as_mut_ptr().cast::<c_void>(), buf.len() as u32)
895 };
896 if n <= 0 {
897 return missing_or_err();
898 }
899 buf.truncate(n as usize);
900 if let Some(nul) = buf.iter().position(|b| *b == 0) {
901 buf.truncate(nul);
902 }
903 let path = PathBuf::from(OsString::from_vec(buf));
904 let id = file_id(&path);
905 Ok(Some(ExeInfo { path, id }))
906 }
907
908 pub fn audit_token(stream: &UnixStream) -> Option<[u32; 8]> {
910 let mut token = [0u32; 8];
911 let mut len = std::mem::size_of_val(&token) as u32;
912 let rc = unsafe {
915 getsockopt(
916 stream.as_raw_fd(),
917 SOL_LOCAL,
918 LOCAL_PEERTOKEN,
919 token.as_mut_ptr().cast::<c_void>(),
920 &mut len,
921 )
922 };
923 (rc == 0 && len as usize == std::mem::size_of_val(&token)).then_some(token)
924 }
925 }
926
927 #[cfg(any(target_os = "linux", target_os = "android"))]
928 mod os {
929 use super::{is_no_such_process, ExeInfo};
930 use std::io;
931 use std::os::unix::fs::MetadataExt;
932 use std::os::unix::net::UnixStream;
933
934 pub const SUPPORTED: bool = true;
935
936 pub fn start_marker(pid: u32) -> io::Result<Option<(u64, u64)>> {
939 let text = match std::fs::read_to_string(format!("/proc/{pid}/stat")) {
940 Ok(text) => text,
941 Err(e) if is_no_such_process(&e) => return Ok(None),
942 Err(e) => return Err(e),
943 };
944 let malformed = || io::Error::new(io::ErrorKind::InvalidData, "unreadable /proc stat");
945 let rest = text.rsplit_once(')').ok_or_else(malformed)?.1;
948 let ticks = rest
949 .split_whitespace()
950 .nth(19)
951 .and_then(|field| field.parse::<u64>().ok())
952 .ok_or_else(malformed)?;
953 Ok(Some((ticks, 0)))
954 }
955
956 pub fn executable_of(pid: u32) -> io::Result<Option<ExeInfo>> {
960 let link = format!("/proc/{pid}/exe");
961 let path = match std::fs::read_link(&link) {
962 Ok(path) => path,
963 Err(e) if is_no_such_process(&e) => return Ok(None),
964 Err(e) => return Err(e),
965 };
966 let id = std::fs::metadata(&link).ok().map(|md| (md.dev(), md.ino()));
967 Ok(Some(ExeInfo { path, id }))
968 }
969
970 pub fn audit_token(_stream: &UnixStream) -> Option<[u32; 8]> {
972 None
973 }
974 }
975
976 #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "android")))]
978 mod os {
979 use super::ExeInfo;
980 use std::io;
981 use std::os::unix::net::UnixStream;
982
983 pub const SUPPORTED: bool = false;
984
985 pub fn start_marker(_pid: u32) -> io::Result<Option<(u64, u64)>> {
986 Err(io::Error::from(io::ErrorKind::Unsupported))
987 }
988 pub fn executable_of(_pid: u32) -> io::Result<Option<ExeInfo>> {
989 Err(io::Error::from(io::ErrorKind::Unsupported))
990 }
991 pub fn audit_token(_stream: &UnixStream) -> Option<[u32; 8]> {
992 None
993 }
994 }
995
996 #[cfg(any(target_os = "linux", target_os = "android"))]
999 fn is_no_such_process(e: &io::Error) -> bool {
1000 e.kind() == io::ErrorKind::NotFound || e.raw_os_error() == Some(3)
1001 }
1002
1003 #[cfg(test)]
1004 mod tests {
1005 use super::*;
1006 use std::os::unix::net::UnixListener;
1007
1008 fn canonical_current_exe() -> PathBuf {
1009 std::fs::canonicalize(std::env::current_exe().unwrap()).unwrap()
1010 }
1011
1012 #[test]
1013 fn the_kernel_describes_this_process_and_a_missing_pid_is_gone_not_an_error() {
1014 let me = std::process::id();
1015 let exe = os::executable_of(me).unwrap().expect("this process exists");
1016 assert_eq!(
1017 std::fs::canonicalize(&exe.path).unwrap(),
1018 canonical_current_exe()
1019 );
1020 let start = os::start_marker(me).unwrap().expect("this process exists");
1021 assert!(start.0 > 0);
1022 assert_eq!(os::start_marker(me).unwrap(), Some(start));
1023
1024 let nobody = i32::MAX as u32;
1025 assert!(os::start_marker(nobody).unwrap().is_none());
1026 assert!(os::executable_of(nobody).unwrap().is_none());
1027 }
1028
1029 #[test]
1030 fn a_listener_in_this_process_is_identified_and_matched_by_its_executable() {
1031 let dir = tempfile::tempdir().unwrap();
1032 let sock = dir.path().join("h.sock");
1033 let _listener = UnixListener::bind(&sock).unwrap();
1034 let stream = UnixStream::connect(&sock).unwrap();
1035
1036 let me = canonical_current_exe();
1037 let host = verify_unix_host(&stream, &HostExpectation::executable(&me))
1038 .unwrap_or_else(|e| panic!("this binary is the host: {e:?}"));
1039 assert_eq!(host.pid, std::process::id());
1040 assert_eq!(host.executable, me);
1041 assert_eq!(host.uid, Some(current_euid()));
1042
1043 let other = dir.path().join("other");
1045 std::fs::write(&other, b"x").unwrap();
1046 let Err(HostConnectError::Identity(e)) =
1047 verify_unix_host(&stream, &HostExpectation::executable(&other))
1048 else {
1049 panic!("a different executable must be refused");
1050 };
1051 assert_eq!(e.reason, HostIdentityReason::ExecutableMismatch);
1052 assert_eq!(e.found_pid, Some(std::process::id()));
1053 assert_eq!(e.found_executable, Some(me.clone()));
1054 assert_eq!(e.expected, vec![other.clone()]);
1055
1056 let any = HostExpectation::any_of([other, dir.path().join("missing"), me]);
1058 assert!(verify_unix_host(&stream, &any).is_ok());
1059 }
1060
1061 #[test]
1062 fn no_expectation_and_a_refusing_verifier_both_fail_closed() {
1063 let dir = tempfile::tempdir().unwrap();
1064 let sock = dir.path().join("h.sock");
1065 let _listener = UnixListener::bind(&sock).unwrap();
1066 let stream = UnixStream::connect(&sock).unwrap();
1067
1068 let Err(HostConnectError::Identity(e)) =
1069 verify_unix_host(&stream, &HostExpectation::any_of(Vec::<PathBuf>::new()))
1070 else {
1071 panic!("an empty expectation must refuse");
1072 };
1073 assert_eq!(e.reason, HostIdentityReason::NoExpectation);
1074
1075 let expect = HostExpectation::executable(canonical_current_exe())
1076 .with_verifier(|host| Err(format!("pid {} is not signed by us", host.pid)));
1077 let Err(HostConnectError::Identity(e)) = verify_unix_host(&stream, &expect) else {
1078 panic!("a refusing verifier must refuse");
1079 };
1080 assert!(
1081 matches!(e.reason, HostIdentityReason::Refused(ref why) if why.contains("not signed"))
1082 );
1083 assert_eq!(e.found_executable, Some(canonical_current_exe()));
1084 }
1085
1086 #[test]
1087 fn a_symlink_a_hard_link_and_an_unknown_path_are_judged_by_the_file_not_the_spelling() {
1088 let dir = tempfile::tempdir().unwrap();
1089 let real = dir.path().join("real");
1090 std::fs::write(&real, b"binary").unwrap();
1091 let found = std::fs::canonicalize(&real).unwrap();
1092 let id = file_id(&found);
1093 assert!(id.is_some());
1094
1095 let link = dir.path().join("link");
1096 std::os::unix::fs::symlink(&real, &link).unwrap();
1097 assert!(same_executable(&link, &found, id));
1098
1099 let hard = dir.path().join("hard");
1100 std::fs::hard_link(&real, &hard).unwrap();
1101 assert!(same_executable(&hard, &found, id), "same file, other name");
1102
1103 let copy = dir.path().join("copy");
1105 std::fs::copy(&real, ©).unwrap();
1106 assert!(!same_executable(©, &found, id));
1107
1108 assert!(!same_executable(&dir.path().join("missing"), &found, id));
1109 assert!(same_executable(&link, &found, None));
1111 assert!(!same_executable(&hard, &found, None));
1112 }
1113
1114 #[cfg(target_os = "macos")]
1118 #[test]
1119 fn a_case_variant_spelling_of_the_expected_path_names_the_same_file() {
1120 let dir = tempfile::tempdir().unwrap();
1121 let real = dir.path().join("hostbin");
1122 std::fs::write(&real, b"binary").unwrap();
1123 let found = std::fs::canonicalize(&real).unwrap();
1124 let id = file_id(&found);
1125 let upper = dir.path().join("HOSTBIN");
1126 if std::fs::metadata(&upper).is_err() {
1127 return;
1128 }
1129 assert!(same_executable(&upper, &found, id));
1130 }
1131 }
1132}
1133
1134#[cfg(windows)]
1135pub(crate) use windows_impl::{verify_pid_host, verify_pipe_host};
1136
1137#[cfg(windows)]
1139mod windows_impl {
1140 use super::*;
1141 use crate::peer::{verify_pipe_server_is_current_user, PeerError, PeerIdentity};
1142 use std::ffi::OsString;
1143 use std::os::windows::ffi::OsStringExt;
1144 use std::os::windows::io::{AsRawHandle, RawHandle};
1145
1146 const PROCESS_QUERY_LIMITED_INFORMATION: u32 = 0x1000;
1147 const SYNCHRONIZE: u32 = 0x0010_0000;
1148 const WAIT_TIMEOUT: u32 = 258;
1149 const ERROR_INVALID_PARAMETER: i32 = 87;
1150 const LONGEST_PATH: usize = 32_768;
1152
1153 #[repr(C)]
1154 #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1155 #[allow(dead_code)]
1156 struct FileTime {
1157 low: u32,
1158 high: u32,
1159 }
1160
1161 #[repr(C)]
1163 #[derive(Default)]
1164 #[allow(dead_code)]
1165 struct ByHandleFileInformation {
1166 attributes: u32,
1167 creation: FileTime,
1168 access: FileTime,
1169 write: FileTime,
1170 volume_serial: u32,
1171 size_high: u32,
1172 size_low: u32,
1173 links: u32,
1174 index_high: u32,
1175 index_low: u32,
1176 }
1177
1178 #[link(name = "kernel32")]
1179 unsafe extern "system" {
1180 fn GetNamedPipeServerProcessId(pipe: RawHandle, pid: *mut u32) -> i32;
1181 fn OpenProcess(access: u32, inherit: i32, pid: u32) -> RawHandle;
1182 fn GetProcessTimes(
1183 process: RawHandle,
1184 creation: *mut FileTime,
1185 exit: *mut FileTime,
1186 kernel: *mut FileTime,
1187 user: *mut FileTime,
1188 ) -> i32;
1189 fn QueryFullProcessImageNameW(
1190 process: RawHandle,
1191 flags: u32,
1192 name: *mut u16,
1193 size: *mut u32,
1194 ) -> i32;
1195 fn WaitForSingleObject(handle: RawHandle, milliseconds: u32) -> u32;
1196 fn CloseHandle(handle: RawHandle) -> i32;
1197 fn GetFileInformationByHandle(file: RawHandle, info: *mut ByHandleFileInformation) -> i32;
1198 }
1199
1200 struct Process(RawHandle);
1202
1203 impl Drop for Process {
1204 fn drop(&mut self) {
1205 unsafe { CloseHandle(self.0) };
1207 }
1208 }
1209
1210 impl Process {
1211 fn open(pid: u32) -> io::Result<Process> {
1212 let handle =
1214 unsafe { OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION | SYNCHRONIZE, 0, pid) };
1215 if handle.is_null() {
1216 return Err(io::Error::last_os_error());
1217 }
1218 Ok(Process(handle))
1219 }
1220
1221 fn created(&self) -> io::Result<FileTime> {
1223 let mut times = [FileTime::default(); 4];
1224 let [creation, exit, kernel, user] = &mut times;
1225 if unsafe { GetProcessTimes(self.0, creation, exit, kernel, user) } == 0 {
1227 return Err(io::Error::last_os_error());
1228 }
1229 Ok(times[0])
1230 }
1231
1232 fn running(&self) -> bool {
1233 unsafe { WaitForSingleObject(self.0, 0) == WAIT_TIMEOUT }
1235 }
1236
1237 fn image(&self) -> io::Result<PathBuf> {
1239 let mut buffer = vec![0u16; LONGEST_PATH];
1240 let mut length = buffer.len() as u32;
1241 if unsafe { QueryFullProcessImageNameW(self.0, 0, buffer.as_mut_ptr(), &mut length) }
1244 == 0
1245 {
1246 return Err(io::Error::last_os_error());
1247 }
1248 Ok(PathBuf::from(OsString::from_wide(
1249 &buffer[..length as usize],
1250 )))
1251 }
1252 }
1253
1254 fn server_pid(pipe: RawHandle) -> io::Result<u32> {
1255 let mut pid = 0u32;
1256 if unsafe { GetNamedPipeServerProcessId(pipe, &mut pid) } == 0 {
1258 return Err(io::Error::last_os_error());
1259 }
1260 Ok(pid)
1261 }
1262
1263 pub(super) fn file_id(path: &Path) -> Option<(u32, u64)> {
1266 let file = std::fs::File::open(path).ok()?;
1267 let mut info = ByHandleFileInformation::default();
1268 if unsafe { GetFileInformationByHandle(file.as_raw_handle(), &mut info) } == 0 {
1270 return None;
1271 }
1272 Some((
1273 info.volume_serial,
1274 (u64::from(info.index_high) << 32) | u64::from(info.index_low),
1275 ))
1276 }
1277
1278 pub(super) fn same_executable(
1282 expected: &Path,
1283 found: &Path,
1284 found_id: Option<(u32, u64)>,
1285 ) -> bool {
1286 let Ok(expected) = std::fs::canonicalize(expected) else {
1287 return false;
1288 };
1289 let lower = |path: &Path| path.to_string_lossy().to_lowercase();
1290 if lower(&expected) == lower(found) {
1291 return true;
1292 }
1293 found_id.is_some() && file_id(&expected) == found_id
1294 }
1295
1296 fn plain(path: PathBuf) -> PathBuf {
1298 let text = path.to_string_lossy();
1299 match text.strip_prefix(r"\\?\UNC\") {
1300 Some(rest) => PathBuf::from(format!(r"\\{rest}")),
1301 None => match text.strip_prefix(r"\\?\") {
1302 Some(rest) => PathBuf::from(rest),
1303 None => path,
1304 },
1305 }
1306 }
1307
1308 pub(crate) fn verify_pipe_host(
1323 pipe: RawHandle,
1324 expect: &HostExpectation,
1325 ) -> Result<HostProcess, HostConnectError> {
1326 let refuse = |pid: Option<u32>, exe: Option<PathBuf>, reason: HostIdentityReason| {
1327 HostConnectError::Identity(HostIdentityError {
1328 expected: expect.executables.clone(),
1329 found_pid: pid,
1330 found_executable: exe,
1331 reason,
1332 })
1333 };
1334 if expect.executables.is_empty() {
1335 return Err(refuse(None, None, HostIdentityReason::NoExpectation));
1336 }
1337
1338 let first = match verify_pipe_server_is_current_user(pipe) {
1339 Ok(id) => id,
1340 Err(e) => {
1341 if e.peer_gone() {
1343 return Err(HostConnectError::Io(io::Error::from(e)));
1344 }
1345 let pid = match &e {
1346 PeerError::Mismatch(id) => id.pid,
1347 _ => None,
1348 };
1349 return Err(refuse(pid, None, HostIdentityReason::Query(e.to_string())));
1350 }
1351 };
1352 let Some(pid) = first.pid else {
1353 return Err(refuse(None, None, HostIdentityReason::PidUnavailable));
1354 };
1355 let unreadable = |what: &str, e: io::Error| {
1356 refuse(
1357 Some(pid),
1358 None,
1359 HostIdentityReason::Unreadable(format!("{what}: {e}")),
1360 )
1361 };
1362 let changed = |why: &str| {
1363 refuse(
1364 Some(pid),
1365 None,
1366 HostIdentityReason::HostChanged(why.to_string()),
1367 )
1368 };
1369
1370 let process = match Process::open(pid) {
1371 Ok(process) => process,
1372 Err(e) if e.raw_os_error() == Some(ERROR_INVALID_PARAMETER) => {
1374 return Err(changed("it exited before it could be opened"))
1375 }
1376 Err(e) => return Err(unreadable("open process", e)),
1377 };
1378 let before = process
1379 .created()
1380 .map_err(|e| unreadable("creation time", e))?;
1381 let image = process
1382 .image()
1383 .map_err(|e| unreadable("executable path", e))?;
1384 let after = process
1385 .created()
1386 .map_err(|e| unreadable("creation time", e))?;
1387 if before != after {
1388 return Err(changed(
1389 "its creation time differed before and after its executable path was read",
1390 ));
1391 }
1392 if !process.running() {
1393 return Err(changed("it exited while it was being identified"));
1394 }
1395 match server_pid(pipe) {
1396 Ok(again) if again == pid => {}
1397 _ => return Err(changed("the pipe no longer reports the same process id")),
1398 }
1399
1400 let found = std::fs::canonicalize(&image).unwrap_or(image);
1401 let id = file_id(&found);
1402 if !expect
1403 .executables
1404 .iter()
1405 .any(|expected| same_executable(expected, &found, id))
1406 {
1407 return Err(refuse(
1408 Some(pid),
1409 Some(plain(found)),
1410 HostIdentityReason::ExecutableMismatch,
1411 ));
1412 }
1413
1414 let host = HostProcess {
1415 pid,
1416 uid: None,
1417 sid: first.sid,
1418 executable: plain(found),
1419 audit_token: None,
1420 };
1421 if let Some(verifier) = &expect.verifier {
1422 if let Err(why) = verifier(&host) {
1423 return Err(refuse(
1424 Some(pid),
1425 Some(host.executable.clone()),
1426 HostIdentityReason::Refused(why),
1427 ));
1428 }
1429 }
1430 drop(process);
1432 Ok(host)
1433 }
1434
1435 pub(super) fn start_marker_of(pid: u32) -> Option<(u64, u64)> {
1436 let t = Process::open(pid).ok()?.created().ok()?;
1437 Some(((u64::from(t.high) << 32) | u64::from(t.low), 0))
1438 }
1439
1440 pub(crate) fn verify_pid_host(
1453 peer: &PeerIdentity,
1454 expect: &HostExpectation,
1455 ) -> Result<HostProcess, HostIdentityError> {
1456 let refuse = |pid: Option<u32>, exe: Option<PathBuf>, reason: HostIdentityReason| {
1457 HostIdentityError {
1458 expected: expect.executables.clone(),
1459 found_pid: pid,
1460 found_executable: exe,
1461 reason,
1462 }
1463 };
1464 if expect.executables.is_empty() {
1465 return Err(refuse(None, None, HostIdentityReason::NoExpectation));
1466 }
1467 let Some(pid) = peer.pid else {
1468 return Err(refuse(None, None, HostIdentityReason::PidUnavailable));
1469 };
1470 let Some(sid) = peer.sid.clone() else {
1471 return Err(refuse(
1472 Some(pid),
1473 None,
1474 HostIdentityReason::Query("no user SID was recorded for the peer".to_string()),
1475 ));
1476 };
1477 let unreadable = |what: &str, e: io::Error| {
1478 refuse(
1479 Some(pid),
1480 None,
1481 HostIdentityReason::Unreadable(format!("{what}: {e}")),
1482 )
1483 };
1484 let changed = |why: &str| {
1485 refuse(
1486 Some(pid),
1487 None,
1488 HostIdentityReason::HostChanged(why.to_string()),
1489 )
1490 };
1491 let process = match Process::open(pid) {
1492 Ok(process) => process,
1493 Err(e) if e.raw_os_error() == Some(ERROR_INVALID_PARAMETER) => {
1494 return Err(changed("it exited before it could be opened"))
1495 }
1496 Err(e) => return Err(unreadable("open process", e)),
1497 };
1498 let before = process
1499 .created()
1500 .map_err(|e| unreadable("creation time", e))?;
1501 let marker = ((u64::from(before.high) << 32) | u64::from(before.low), 0);
1503 if peer.started != Some(marker) {
1504 return Err(changed(concat!(
1505 "the process is not the one that connected (its creation time differs ",
1506 "from the one recorded at connect, or none was recorded)"
1507 )));
1508 }
1509 let image = process
1510 .image()
1511 .map_err(|e| unreadable("executable path", e))?;
1512 let after = process
1513 .created()
1514 .map_err(|e| unreadable("creation time", e))?;
1515 if before != after {
1516 return Err(changed(
1517 "its creation time differed before and after its executable path was read",
1518 ));
1519 }
1520 if !process.running() {
1521 return Err(changed("it exited while it was being identified"));
1522 }
1523
1524 let found = std::fs::canonicalize(&image).unwrap_or(image);
1525 let id = file_id(&found);
1526 if !expect
1527 .executables
1528 .iter()
1529 .any(|expected| same_executable(expected, &found, id))
1530 {
1531 return Err(refuse(
1532 Some(pid),
1533 Some(plain(found)),
1534 HostIdentityReason::ExecutableMismatch,
1535 ));
1536 }
1537 let host = HostProcess {
1538 pid,
1539 uid: None,
1540 sid: Some(sid),
1541 executable: plain(found),
1542 audit_token: None,
1543 };
1544 if let Some(verifier) = &expect.verifier {
1545 if let Err(why) = verifier(&host) {
1546 return Err(refuse(
1547 Some(pid),
1548 Some(host.executable.clone()),
1549 HostIdentityReason::Refused(why),
1550 ));
1551 }
1552 }
1553 drop(process);
1555 Ok(host)
1556 }
1557
1558 #[cfg(test)]
1559 mod tests {
1560 use super::*;
1561
1562 #[test]
1563 fn the_same_file_matches_by_path_in_any_case_and_by_identity_through_a_hard_link() {
1564 let dir = tempfile::tempdir().unwrap();
1565 let real = dir.path().join("HostBin.exe");
1566 std::fs::write(&real, b"binary").unwrap();
1567 let found = std::fs::canonicalize(&real).unwrap();
1568 let id = file_id(&found);
1569 assert!(id.is_some());
1570
1571 assert!(same_executable(&real, &found, id));
1572 assert!(same_executable(&dir.path().join("hostbin.EXE"), &found, id));
1574 assert!(same_executable(
1575 &dir.path().join("hostbin.EXE"),
1576 &found,
1577 None
1578 ));
1579
1580 let hard = dir.path().join("hard.exe");
1581 std::fs::hard_link(&real, &hard).unwrap();
1582 assert!(same_executable(&hard, &found, id), "same file, other name");
1583 assert!(!same_executable(&hard, &found, None));
1585
1586 let copy = dir.path().join("copy.exe");
1588 std::fs::copy(&real, ©).unwrap();
1589 assert!(!same_executable(©, &found, id));
1590 assert!(!same_executable(
1591 &dir.path().join("missing.exe"),
1592 &found,
1593 id
1594 ));
1595 }
1596
1597 #[test]
1598 fn this_process_is_described_by_its_own_handle() {
1599 let process = Process::open(std::process::id()).unwrap();
1600 assert!(process.running());
1601 assert_eq!(process.created().unwrap(), process.created().unwrap());
1602 let image = std::fs::canonicalize(process.image().unwrap()).unwrap();
1603 let me = std::fs::canonicalize(std::env::current_exe().unwrap()).unwrap();
1604 assert!(same_executable(&me, &image, file_id(&image)));
1605 assert!(!plain(image).to_string_lossy().starts_with(r"\\?\"));
1606 }
1607
1608 #[test]
1609 fn a_process_id_nothing_holds_cannot_be_opened() {
1610 assert!(Process::open(0xFFFF_FFF3).is_err());
1612 }
1613 }
1614}
1615
1616#[cfg(test)]
1617mod tests {
1618 use super::*;
1619
1620 #[test]
1621 fn a_bare_file_name_is_one_component() {
1622 assert!(is_single_component(OsStr::new("mailright")));
1623 assert!(is_single_component(OsStr::new("mailright.exe")));
1624 assert!(!is_single_component(OsStr::new("")));
1625 assert!(!is_single_component(OsStr::new("..")));
1626 assert!(!is_single_component(OsStr::new("a/b")));
1627 assert!(!is_single_component(OsStr::new("/abs")));
1628 }
1629
1630 #[test]
1631 fn install_relative_paths_are_derived_from_where_the_engine_lives() {
1632 let engine = Path::new("/Applications/MailRight.app/Contents/Resources/bin/engine");
1633 assert_eq!(
1634 bundle_main(engine, OsStr::new("mailright")),
1635 Some(PathBuf::from(
1636 "/Applications/MailRight.app/Contents/MacOS/mailright"
1637 ))
1638 );
1639 assert_eq!(
1640 up_and_join(engine, 1, Path::new("app")),
1641 Some(PathBuf::from(
1642 "/Applications/MailRight.app/Contents/Resources/bin/app"
1643 ))
1644 );
1645 assert_eq!(
1646 up_and_join(engine, 3, Path::new("MacOS/mailright")),
1647 Some(PathBuf::from(
1648 "/Applications/MailRight.app/Contents/MacOS/mailright"
1649 ))
1650 );
1651 assert_eq!(
1653 bundle_main(Path::new("/opt/app/bin/engine"), OsStr::new("x")),
1654 None
1655 );
1656 assert_eq!(up_and_join(engine, 0, Path::new("x")), None);
1657 assert_eq!(up_and_join(engine, 1, Path::new("/etc/passwd")), None);
1658 #[cfg(windows)]
1660 for escape in [r"\Windows\System32\cmd.exe", "C:cmd.exe", r"C:\cmd.exe"] {
1661 assert_eq!(up_and_join(engine, 1, Path::new(escape)), None, "{escape}");
1662 }
1663 assert_eq!(up_and_join(Path::new("engine"), 5, Path::new("x")), None);
1664 }
1665
1666 #[test]
1667 fn helpers_resolve_against_the_running_test_binary() {
1668 let exe = std::fs::canonicalize(std::env::current_exe().unwrap()).unwrap();
1669 let sibling = HostExpectation::sibling_executable("host-binary").unwrap();
1670 assert_eq!(
1671 sibling.executables(),
1672 [exe.parent().unwrap().join("host-binary")]
1673 );
1674 assert!(HostExpectation::sibling_executable("a/b").is_err());
1675 assert!(HostExpectation::sibling_executable("..").is_err());
1676 assert!(HostExpectation::bundle_executable("x/y").is_err());
1677 let up2 = HostExpectation::relative_to_current_exe(2, "bin/host").unwrap();
1678 assert_eq!(
1679 up2.executables(),
1680 [exe.parent().unwrap().parent().unwrap().join("bin/host")]
1681 );
1682 assert!(HostExpectation::relative_to_current_exe(0, "x").is_err());
1683 }
1684
1685 #[test]
1686 fn the_error_says_what_was_expected_and_what_was_found() {
1687 let e = HostIdentityError {
1688 expected: vec![PathBuf::from("/a/app"), PathBuf::from("/b/app")],
1689 found_pid: Some(42),
1690 found_executable: Some(PathBuf::from("/tmp/evil")),
1691 reason: HostIdentityReason::ExecutableMismatch,
1692 };
1693 let text = e.to_string();
1694 assert!(
1695 text.contains("pid 42") && text.contains("/tmp/evil"),
1696 "{text}"
1697 );
1698 assert!(text.contains("/a/app or /b/app"), "{text}");
1699 let none = HostIdentityError {
1700 expected: Vec::new(),
1701 found_pid: None,
1702 found_executable: None,
1703 reason: HostIdentityReason::NoExpectation,
1704 };
1705 assert!(none.to_string().contains("none supplied"));
1706 }
1707
1708 #[test]
1709 fn a_refusal_collapses_to_a_non_retryable_wire_error() {
1710 let e = HostIdentityError {
1711 expected: vec![PathBuf::from("/a/app")],
1712 found_pid: Some(7),
1713 found_executable: None,
1714 reason: HostIdentityReason::PidUnavailable,
1715 };
1716 let wire: crate::ServiceError = e.clone().into();
1717 assert_eq!(wire.code, crate::ERROR_HOST_IDENTITY);
1718 assert!(!wire.retryable);
1719 assert!(wire.message.contains("pid 7"));
1720 let c: crate::ConnectError = e.into();
1721 assert_eq!(c.into_service_error(), wire);
1722 }
1723}