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}
74
75impl fmt::Display for HostIdentityReason {
76 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
77 match self {
78 HostIdentityReason::NoExpectation => {
79 write!(
80 f,
81 "no expected host executable was supplied, so no host can match"
82 )
83 }
84 HostIdentityReason::UidMismatch { host, client } => match host {
85 Some(host) => write!(
86 f,
87 "the host runs as uid {host}, this client as uid {client}"
88 ),
89 None => write!(
90 f,
91 "the host's uid is unknown, this client runs as uid {client}"
92 ),
93 },
94 HostIdentityReason::PidUnavailable => {
95 write!(f, "the OS did not report the host's process id")
96 }
97 HostIdentityReason::Unreadable(why) => {
98 write!(f, "the OS would not describe the host process ({why})")
99 }
100 HostIdentityReason::ExecutableMismatch => {
101 write!(
102 f,
103 "the host's executable is not one of the expected executables"
104 )
105 }
106 HostIdentityReason::HostChanged(why) => {
107 write!(
108 f,
109 "the host process changed while it was identified ({why})"
110 )
111 }
112 HostIdentityReason::Refused(why) => {
113 write!(f, "the caller's extra host verification refused it ({why})")
114 }
115 HostIdentityReason::Unverified { platform, detail } => {
116 write!(
117 f,
118 "host verification is unavailable on {platform} ({detail})"
119 )
120 }
121 HostIdentityReason::Query(why) => {
122 write!(f, "the OS could not identify the peer ({why})")
123 }
124 }
125 }
126}
127
128#[derive(Debug, Clone, PartialEq, Eq)]
135#[non_exhaustive]
136pub struct HostIdentityError {
137 pub expected: Vec<PathBuf>,
139 pub found_pid: Option<u32>,
141 pub found_executable: Option<PathBuf>,
143 pub reason: HostIdentityReason,
144}
145
146impl fmt::Display for HostIdentityError {
147 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
148 write!(
149 f,
150 "the service host failed identity verification: {}",
151 self.reason
152 )?;
153 if let Some(pid) = self.found_pid {
154 write!(f, "; host pid {pid}")?;
155 }
156 if let Some(exe) = &self.found_executable {
157 write!(f, "; host executable {}", exe.display())?;
158 }
159 if self.expected.is_empty() {
160 write!(f, "; expected: none supplied")
161 } else {
162 write!(f, "; expected ")?;
163 for (i, path) in self.expected.iter().enumerate() {
164 if i > 0 {
165 write!(f, " or ")?;
166 }
167 write!(f, "{}", path.display())?;
168 }
169 Ok(())
170 }
171 }
172}
173
174impl std::error::Error for HostIdentityError {}
175
176#[derive(Debug)]
178pub(crate) enum HostConnectError {
179 Io(io::Error),
181 Identity(HostIdentityError),
183}
184
185type Verifier = Arc<dyn Fn(&HostProcess) -> Result<(), String> + Send + Sync>;
186
187#[derive(Clone, Default)]
205pub struct HostExpectation {
206 executables: Vec<PathBuf>,
207 verifier: Option<Verifier>,
208}
209
210impl fmt::Debug for HostExpectation {
211 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
212 f.debug_struct("HostExpectation")
213 .field("executables", &self.executables)
214 .field("verifier", &self.verifier.as_ref().map(|_| "<fn>"))
215 .finish()
216 }
217}
218
219fn invalid(what: &str) -> io::Error {
220 io::Error::new(io::ErrorKind::InvalidInput, what.to_string())
221}
222
223fn current_exe_canonical() -> io::Result<PathBuf> {
224 std::fs::canonicalize(std::env::current_exe()?)
225}
226
227fn is_single_component(name: &OsStr) -> bool {
229 let path = Path::new(name);
230 path.file_name() == Some(name) && path.components().count() == 1
231}
232
233fn up_and_join(exe: &Path, levels_up: usize, relative: &Path) -> Option<PathBuf> {
238 use std::path::Component;
239 let leaves_base = relative
240 .components()
241 .any(|part| matches!(part, Component::Prefix(_) | Component::RootDir));
242 if levels_up == 0 || relative.is_absolute() || leaves_base {
243 return None;
244 }
245 exe.ancestors()
246 .nth(levels_up)
247 .map(|base| base.join(relative))
248}
249
250fn bundle_main(exe: &Path, file_name: &OsStr) -> Option<PathBuf> {
252 exe.ancestors()
253 .skip(1)
254 .find(|dir| dir.extension() == Some(OsStr::new("app")))
255 .map(|bundle| bundle.join("Contents").join("MacOS").join(file_name))
256}
257
258impl HostExpectation {
259 pub fn executable(path: impl Into<PathBuf>) -> Self {
261 Self {
262 executables: vec![path.into()],
263 verifier: None,
264 }
265 }
266
267 pub fn any_of<I, P>(paths: I) -> Self
269 where
270 I: IntoIterator<Item = P>,
271 P: Into<PathBuf>,
272 {
273 Self {
274 executables: paths.into_iter().map(Into::into).collect(),
275 verifier: None,
276 }
277 }
278
279 pub fn or_executable(mut self, path: impl Into<PathBuf>) -> Self {
281 self.executables.push(path.into());
282 self
283 }
284
285 pub fn sibling_executable(file_name: impl AsRef<OsStr>) -> io::Result<Self> {
289 let name = file_name.as_ref();
290 if !is_single_component(name) {
291 return Err(invalid("the host file name must be a bare file name"));
292 }
293 Self::relative_to_current_exe(1, name)
294 }
295
296 pub fn bundle_executable(file_name: impl AsRef<OsStr>) -> io::Result<Self> {
302 let name = file_name.as_ref();
303 if !is_single_component(name) {
304 return Err(invalid("the host file name must be a bare file name"));
305 }
306 let exe = current_exe_canonical()?;
307 let path = bundle_main(&exe, name).ok_or_else(|| {
308 io::Error::new(
309 io::ErrorKind::NotFound,
310 "the running executable is not inside an .app bundle",
311 )
312 })?;
313 Ok(Self::executable(path))
314 }
315
316 pub fn relative_to_current_exe(
321 levels_up: usize,
322 relative: impl AsRef<Path>,
323 ) -> io::Result<Self> {
324 let exe = current_exe_canonical()?;
325 let path = up_and_join(&exe, levels_up, relative.as_ref()).ok_or_else(|| {
326 invalid("no such directory above the running executable, or the path is absolute")
327 })?;
328 Ok(Self::executable(path))
329 }
330
331 pub fn with_verifier(
343 mut self,
344 check: impl Fn(&HostProcess) -> Result<(), String> + Send + Sync + 'static,
345 ) -> Self {
346 self.verifier = Some(Arc::new(check));
347 self
348 }
349
350 pub fn executables(&self) -> &[PathBuf] {
352 &self.executables
353 }
354}
355
356#[cfg(unix)]
357pub(crate) use unix_impl::verify_unix_host;
358
359#[cfg(unix)]
360mod unix_impl {
361 use super::{
362 HostConnectError, HostExpectation, HostIdentityError, HostIdentityReason, HostProcess,
363 };
364 use crate::peer::{current_euid, verify_peer_is_current_user, PeerError};
365 use std::io;
366 use std::os::unix::fs::MetadataExt;
367 use std::os::unix::net::UnixStream;
368 use std::path::{Path, PathBuf};
369
370 pub type FileId = (u64, u64);
372
373 pub fn file_id(path: &Path) -> Option<FileId> {
374 let md = std::fs::metadata(path).ok()?;
375 Some((md.dev(), md.ino()))
376 }
377
378 pub struct ExeInfo {
380 pub path: PathBuf,
381 pub id: Option<FileId>,
384 }
385
386 #[derive(PartialEq, Eq)]
389 struct Witness {
390 start: (u64, u64),
391 token: Option<[u32; 8]>,
392 }
393
394 enum ObserveError {
395 Gone,
396 Unreadable(String),
397 }
398
399 impl From<ObserveError> for HostIdentityReason {
400 fn from(e: ObserveError) -> Self {
401 match e {
402 ObserveError::Gone => HostIdentityReason::HostChanged(
403 "it exited before it could be identified".to_string(),
404 ),
405 ObserveError::Unreadable(why) => HostIdentityReason::Unreadable(why),
406 }
407 }
408 }
409
410 #[cfg(target_os = "macos")]
413 fn missing_or_err<T>() -> io::Result<Option<T>> {
414 let e = io::Error::last_os_error();
415 match e.raw_os_error() {
416 Some(3) | Some(0) | None => Ok(None),
417 _ => Err(e),
418 }
419 }
420
421 fn witness(stream: &UnixStream, pid: u32) -> Result<Witness, ObserveError> {
422 let token = os::audit_token(stream);
423 let start = match os::start_marker(pid) {
424 Ok(Some(start)) => start,
425 Ok(None) => return Err(ObserveError::Gone),
426 Err(e) => return Err(ObserveError::Unreadable(format!("process start time: {e}"))),
427 };
428 Ok(Witness { start, token })
429 }
430
431 pub fn same_executable(expected: &Path, found: &Path, found_id: Option<FileId>) -> bool {
437 let Ok(canonical) = std::fs::canonicalize(expected) else {
438 return false;
439 };
440 if canonical == found {
441 return true;
442 }
443 match (found_id, file_id(&canonical)) {
444 (Some(running), Some(named)) => running == named,
445 _ => false,
446 }
447 }
448
449 pub(crate) fn verify_unix_host(
461 stream: &UnixStream,
462 expect: &HostExpectation,
463 ) -> Result<HostProcess, HostConnectError> {
464 let refuse = |pid: Option<u32>, exe: Option<PathBuf>, reason: HostIdentityReason| {
465 HostConnectError::Identity(HostIdentityError {
466 expected: expect.executables.clone(),
467 found_pid: pid,
468 found_executable: exe,
469 reason,
470 })
471 };
472 if expect.executables.is_empty() {
473 return Err(refuse(None, None, HostIdentityReason::NoExpectation));
474 }
475 if !os::SUPPORTED {
476 return Err(refuse(
477 None,
478 None,
479 HostIdentityReason::Unverified {
480 platform: std::env::consts::OS,
481 detail: "reading a process's executable path is not implemented for this Unix",
482 },
483 ));
484 }
485
486 let first = match verify_peer_is_current_user(stream) {
487 Ok(id) => id,
488 Err(PeerError::Mismatch(id)) => {
489 return Err(refuse(
490 id.pid,
491 None,
492 HostIdentityReason::UidMismatch {
493 host: id.uid,
494 client: current_euid(),
495 },
496 ))
497 }
498 Err(PeerError::Unsupported(detail)) => {
499 return Err(refuse(
500 None,
501 None,
502 HostIdentityReason::Unverified {
503 platform: std::env::consts::OS,
504 detail,
505 },
506 ))
507 }
508 Err(e) => {
509 if e.peer_gone() {
511 return Err(HostConnectError::Io(io::Error::from(e)));
512 }
513 return Err(refuse(None, None, HostIdentityReason::Query(e.to_string())));
514 }
515 };
516 let Some(pid) = first.pid else {
517 return Err(refuse(None, None, HostIdentityReason::PidUnavailable));
518 };
519
520 let observe = |e: ObserveError| refuse(Some(pid), None, e.into());
521 let changed = |why: &str| {
522 refuse(
523 Some(pid),
524 None,
525 HostIdentityReason::HostChanged(why.to_string()),
526 )
527 };
528
529 let before = witness(stream, pid).map_err(&observe)?;
530 let exe = match os::executable_of(pid) {
531 Ok(Some(exe)) => exe,
532 Ok(None) => return Err(observe(ObserveError::Gone)),
533 Err(e) => {
534 return Err(observe(ObserveError::Unreadable(format!(
535 "executable path: {e}"
536 ))))
537 }
538 };
539 let after = witness(stream, pid).map_err(&observe)?;
540 if before != after {
541 return Err(changed(
542 "its identity differed before and after its executable path was read",
543 ));
544 }
545 match verify_peer_is_current_user(stream) {
546 Ok(again) if again.pid == Some(pid) => {}
547 _ => return Err(changed("the socket no longer reports the same process id")),
548 }
549
550 let ExeInfo { path, id } = exe;
551 let found = std::fs::canonicalize(&path).unwrap_or(path);
552 if !expect
553 .executables
554 .iter()
555 .any(|expected| same_executable(expected, &found, id))
556 {
557 return Err(refuse(
558 Some(pid),
559 Some(found),
560 HostIdentityReason::ExecutableMismatch,
561 ));
562 }
563
564 let host = HostProcess {
565 pid,
566 uid: Some(current_euid()),
567 sid: None,
568 executable: found,
569 audit_token: after.token,
570 };
571 if let Some(verifier) = &expect.verifier {
572 if let Err(why) = verifier(&host) {
573 return Err(refuse(
574 Some(pid),
575 Some(host.executable.clone()),
576 HostIdentityReason::Refused(why),
577 ));
578 }
579 }
580 Ok(host)
581 }
582
583 #[cfg(target_os = "macos")]
584 mod os {
585 use super::{file_id, missing_or_err, ExeInfo};
586 use std::ffi::{c_void, OsString};
587 use std::io;
588 use std::os::fd::AsRawFd;
589 use std::os::unix::ffi::OsStringExt;
590 use std::os::unix::net::UnixStream;
591 use std::path::PathBuf;
592
593 pub const SUPPORTED: bool = true;
594
595 #[repr(C)]
598 #[allow(dead_code)]
599 struct ProcBsdInfo {
600 pbi_flags: u32,
601 pbi_status: u32,
602 pbi_xstatus: u32,
603 pbi_pid: u32,
604 pbi_ppid: u32,
605 pbi_uid: u32,
606 pbi_gid: u32,
607 pbi_ruid: u32,
608 pbi_rgid: u32,
609 pbi_svuid: u32,
610 pbi_svgid: u32,
611 rfu_1: u32,
612 pbi_comm: [u8; 16],
613 pbi_name: [u8; 32],
614 pbi_nfiles: u32,
615 pbi_pgid: u32,
616 pbi_pjobc: u32,
617 e_tdev: u32,
618 e_tpgid: u32,
619 pbi_nice: i32,
620 pbi_start_tvsec: u64,
621 pbi_start_tvusec: u64,
622 }
623 const _: () = assert!(std::mem::size_of::<ProcBsdInfo>() == 136);
624
625 const PROC_PIDTBSDINFO: i32 = 3;
626 const PROC_PIDPATHINFO_MAXSIZE: usize = 4 * 1024;
627 const SOL_LOCAL: i32 = 0;
628 const LOCAL_PEERTOKEN: i32 = 0x006;
629
630 extern "C" {
631 fn proc_pidinfo(
632 pid: i32,
633 flavor: i32,
634 arg: u64,
635 buffer: *mut c_void,
636 buffersize: i32,
637 ) -> i32;
638 fn proc_pidpath(pid: i32, buffer: *mut c_void, buffersize: u32) -> i32;
639 fn getsockopt(fd: i32, level: i32, name: i32, val: *mut c_void, len: *mut u32) -> i32;
640 }
641
642 fn to_pid(pid: u32) -> io::Result<i32> {
643 i32::try_from(pid)
644 .ok()
645 .filter(|p| *p > 0)
646 .ok_or_else(|| io::Error::from(io::ErrorKind::InvalidInput))
647 }
648
649 pub fn start_marker(pid: u32) -> io::Result<Option<(u64, u64)>> {
652 let raw_pid = to_pid(pid)?;
653 let mut info = std::mem::MaybeUninit::<ProcBsdInfo>::zeroed();
654 let size = std::mem::size_of::<ProcBsdInfo>() as i32;
655 let written = unsafe {
657 proc_pidinfo(
658 raw_pid,
659 PROC_PIDTBSDINFO,
660 0,
661 info.as_mut_ptr().cast::<c_void>(),
662 size,
663 )
664 };
665 if written != size {
666 return missing_or_err();
667 }
668 let info = unsafe { info.assume_init() };
671 Ok((info.pbi_pid == pid).then_some((info.pbi_start_tvsec, info.pbi_start_tvusec)))
672 }
673
674 pub fn executable_of(pid: u32) -> io::Result<Option<ExeInfo>> {
677 let raw_pid = to_pid(pid)?;
678 let mut buf = vec![0u8; PROC_PIDPATHINFO_MAXSIZE];
679 let n = unsafe {
681 proc_pidpath(raw_pid, buf.as_mut_ptr().cast::<c_void>(), buf.len() as u32)
682 };
683 if n <= 0 {
684 return missing_or_err();
685 }
686 buf.truncate(n as usize);
687 if let Some(nul) = buf.iter().position(|b| *b == 0) {
688 buf.truncate(nul);
689 }
690 let path = PathBuf::from(OsString::from_vec(buf));
691 let id = file_id(&path);
692 Ok(Some(ExeInfo { path, id }))
693 }
694
695 pub fn audit_token(stream: &UnixStream) -> Option<[u32; 8]> {
697 let mut token = [0u32; 8];
698 let mut len = std::mem::size_of_val(&token) as u32;
699 let rc = unsafe {
702 getsockopt(
703 stream.as_raw_fd(),
704 SOL_LOCAL,
705 LOCAL_PEERTOKEN,
706 token.as_mut_ptr().cast::<c_void>(),
707 &mut len,
708 )
709 };
710 (rc == 0 && len as usize == std::mem::size_of_val(&token)).then_some(token)
711 }
712 }
713
714 #[cfg(any(target_os = "linux", target_os = "android"))]
715 mod os {
716 use super::{is_no_such_process, ExeInfo};
717 use std::io;
718 use std::os::unix::fs::MetadataExt;
719 use std::os::unix::net::UnixStream;
720
721 pub const SUPPORTED: bool = true;
722
723 pub fn start_marker(pid: u32) -> io::Result<Option<(u64, u64)>> {
726 let text = match std::fs::read_to_string(format!("/proc/{pid}/stat")) {
727 Ok(text) => text,
728 Err(e) if is_no_such_process(&e) => return Ok(None),
729 Err(e) => return Err(e),
730 };
731 let malformed = || io::Error::new(io::ErrorKind::InvalidData, "unreadable /proc stat");
732 let rest = text.rsplit_once(')').ok_or_else(malformed)?.1;
735 let ticks = rest
736 .split_whitespace()
737 .nth(19)
738 .and_then(|field| field.parse::<u64>().ok())
739 .ok_or_else(malformed)?;
740 Ok(Some((ticks, 0)))
741 }
742
743 pub fn executable_of(pid: u32) -> io::Result<Option<ExeInfo>> {
747 let link = format!("/proc/{pid}/exe");
748 let path = match std::fs::read_link(&link) {
749 Ok(path) => path,
750 Err(e) if is_no_such_process(&e) => return Ok(None),
751 Err(e) => return Err(e),
752 };
753 let id = std::fs::metadata(&link).ok().map(|md| (md.dev(), md.ino()));
754 Ok(Some(ExeInfo { path, id }))
755 }
756
757 pub fn audit_token(_stream: &UnixStream) -> Option<[u32; 8]> {
759 None
760 }
761 }
762
763 #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "android")))]
765 mod os {
766 use super::ExeInfo;
767 use std::io;
768 use std::os::unix::net::UnixStream;
769
770 pub const SUPPORTED: bool = false;
771
772 pub fn start_marker(_pid: u32) -> io::Result<Option<(u64, u64)>> {
773 Err(io::Error::from(io::ErrorKind::Unsupported))
774 }
775 pub fn executable_of(_pid: u32) -> io::Result<Option<ExeInfo>> {
776 Err(io::Error::from(io::ErrorKind::Unsupported))
777 }
778 pub fn audit_token(_stream: &UnixStream) -> Option<[u32; 8]> {
779 None
780 }
781 }
782
783 #[cfg(any(target_os = "linux", target_os = "android"))]
786 fn is_no_such_process(e: &io::Error) -> bool {
787 e.kind() == io::ErrorKind::NotFound || e.raw_os_error() == Some(3)
788 }
789
790 #[cfg(test)]
791 mod tests {
792 use super::*;
793 use std::os::unix::net::UnixListener;
794
795 fn canonical_current_exe() -> PathBuf {
796 std::fs::canonicalize(std::env::current_exe().unwrap()).unwrap()
797 }
798
799 #[test]
800 fn the_kernel_describes_this_process_and_a_missing_pid_is_gone_not_an_error() {
801 let me = std::process::id();
802 let exe = os::executable_of(me).unwrap().expect("this process exists");
803 assert_eq!(
804 std::fs::canonicalize(&exe.path).unwrap(),
805 canonical_current_exe()
806 );
807 let start = os::start_marker(me).unwrap().expect("this process exists");
808 assert!(start.0 > 0);
809 assert_eq!(os::start_marker(me).unwrap(), Some(start));
810
811 let nobody = i32::MAX as u32;
812 assert!(os::start_marker(nobody).unwrap().is_none());
813 assert!(os::executable_of(nobody).unwrap().is_none());
814 }
815
816 #[test]
817 fn a_listener_in_this_process_is_identified_and_matched_by_its_executable() {
818 let dir = tempfile::tempdir().unwrap();
819 let sock = dir.path().join("h.sock");
820 let _listener = UnixListener::bind(&sock).unwrap();
821 let stream = UnixStream::connect(&sock).unwrap();
822
823 let me = canonical_current_exe();
824 let host = verify_unix_host(&stream, &HostExpectation::executable(&me))
825 .unwrap_or_else(|e| panic!("this binary is the host: {e:?}"));
826 assert_eq!(host.pid, std::process::id());
827 assert_eq!(host.executable, me);
828 assert_eq!(host.uid, Some(current_euid()));
829
830 let other = dir.path().join("other");
832 std::fs::write(&other, b"x").unwrap();
833 let Err(HostConnectError::Identity(e)) =
834 verify_unix_host(&stream, &HostExpectation::executable(&other))
835 else {
836 panic!("a different executable must be refused");
837 };
838 assert_eq!(e.reason, HostIdentityReason::ExecutableMismatch);
839 assert_eq!(e.found_pid, Some(std::process::id()));
840 assert_eq!(e.found_executable, Some(me.clone()));
841 assert_eq!(e.expected, vec![other.clone()]);
842
843 let any = HostExpectation::any_of([other, dir.path().join("missing"), me]);
845 assert!(verify_unix_host(&stream, &any).is_ok());
846 }
847
848 #[test]
849 fn no_expectation_and_a_refusing_verifier_both_fail_closed() {
850 let dir = tempfile::tempdir().unwrap();
851 let sock = dir.path().join("h.sock");
852 let _listener = UnixListener::bind(&sock).unwrap();
853 let stream = UnixStream::connect(&sock).unwrap();
854
855 let Err(HostConnectError::Identity(e)) =
856 verify_unix_host(&stream, &HostExpectation::any_of(Vec::<PathBuf>::new()))
857 else {
858 panic!("an empty expectation must refuse");
859 };
860 assert_eq!(e.reason, HostIdentityReason::NoExpectation);
861
862 let expect = HostExpectation::executable(canonical_current_exe())
863 .with_verifier(|host| Err(format!("pid {} is not signed by us", host.pid)));
864 let Err(HostConnectError::Identity(e)) = verify_unix_host(&stream, &expect) else {
865 panic!("a refusing verifier must refuse");
866 };
867 assert!(
868 matches!(e.reason, HostIdentityReason::Refused(ref why) if why.contains("not signed"))
869 );
870 assert_eq!(e.found_executable, Some(canonical_current_exe()));
871 }
872
873 #[test]
874 fn a_symlink_a_hard_link_and_an_unknown_path_are_judged_by_the_file_not_the_spelling() {
875 let dir = tempfile::tempdir().unwrap();
876 let real = dir.path().join("real");
877 std::fs::write(&real, b"binary").unwrap();
878 let found = std::fs::canonicalize(&real).unwrap();
879 let id = file_id(&found);
880 assert!(id.is_some());
881
882 let link = dir.path().join("link");
883 std::os::unix::fs::symlink(&real, &link).unwrap();
884 assert!(same_executable(&link, &found, id));
885
886 let hard = dir.path().join("hard");
887 std::fs::hard_link(&real, &hard).unwrap();
888 assert!(same_executable(&hard, &found, id), "same file, other name");
889
890 let copy = dir.path().join("copy");
892 std::fs::copy(&real, ©).unwrap();
893 assert!(!same_executable(©, &found, id));
894
895 assert!(!same_executable(&dir.path().join("missing"), &found, id));
896 assert!(same_executable(&link, &found, None));
898 assert!(!same_executable(&hard, &found, None));
899 }
900
901 #[cfg(target_os = "macos")]
905 #[test]
906 fn a_case_variant_spelling_of_the_expected_path_names_the_same_file() {
907 let dir = tempfile::tempdir().unwrap();
908 let real = dir.path().join("hostbin");
909 std::fs::write(&real, b"binary").unwrap();
910 let found = std::fs::canonicalize(&real).unwrap();
911 let id = file_id(&found);
912 let upper = dir.path().join("HOSTBIN");
913 if std::fs::metadata(&upper).is_err() {
914 return;
915 }
916 assert!(same_executable(&upper, &found, id));
917 }
918 }
919}
920
921#[cfg(windows)]
922pub(crate) use windows_impl::verify_pipe_host;
923
924#[cfg(windows)]
926mod windows_impl {
927 use super::*;
928 use crate::peer::{verify_pipe_server_is_current_user, PeerError};
929 use std::ffi::OsString;
930 use std::os::windows::ffi::OsStringExt;
931 use std::os::windows::io::{AsRawHandle, RawHandle};
932
933 const PROCESS_QUERY_LIMITED_INFORMATION: u32 = 0x1000;
934 const SYNCHRONIZE: u32 = 0x0010_0000;
935 const WAIT_TIMEOUT: u32 = 258;
936 const ERROR_INVALID_PARAMETER: i32 = 87;
937 const LONGEST_PATH: usize = 32_768;
939
940 #[repr(C)]
941 #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
942 #[allow(dead_code)]
943 struct FileTime {
944 low: u32,
945 high: u32,
946 }
947
948 #[repr(C)]
950 #[derive(Default)]
951 #[allow(dead_code)]
952 struct ByHandleFileInformation {
953 attributes: u32,
954 creation: FileTime,
955 access: FileTime,
956 write: FileTime,
957 volume_serial: u32,
958 size_high: u32,
959 size_low: u32,
960 links: u32,
961 index_high: u32,
962 index_low: u32,
963 }
964
965 #[link(name = "kernel32")]
966 unsafe extern "system" {
967 fn GetNamedPipeServerProcessId(pipe: RawHandle, pid: *mut u32) -> i32;
968 fn OpenProcess(access: u32, inherit: i32, pid: u32) -> RawHandle;
969 fn GetProcessTimes(
970 process: RawHandle,
971 creation: *mut FileTime,
972 exit: *mut FileTime,
973 kernel: *mut FileTime,
974 user: *mut FileTime,
975 ) -> i32;
976 fn QueryFullProcessImageNameW(
977 process: RawHandle,
978 flags: u32,
979 name: *mut u16,
980 size: *mut u32,
981 ) -> i32;
982 fn WaitForSingleObject(handle: RawHandle, milliseconds: u32) -> u32;
983 fn CloseHandle(handle: RawHandle) -> i32;
984 fn GetFileInformationByHandle(file: RawHandle, info: *mut ByHandleFileInformation) -> i32;
985 }
986
987 struct Process(RawHandle);
989
990 impl Drop for Process {
991 fn drop(&mut self) {
992 unsafe { CloseHandle(self.0) };
994 }
995 }
996
997 impl Process {
998 fn open(pid: u32) -> io::Result<Process> {
999 let handle =
1001 unsafe { OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION | SYNCHRONIZE, 0, pid) };
1002 if handle.is_null() {
1003 return Err(io::Error::last_os_error());
1004 }
1005 Ok(Process(handle))
1006 }
1007
1008 fn created(&self) -> io::Result<FileTime> {
1010 let mut times = [FileTime::default(); 4];
1011 let [creation, exit, kernel, user] = &mut times;
1012 if unsafe { GetProcessTimes(self.0, creation, exit, kernel, user) } == 0 {
1014 return Err(io::Error::last_os_error());
1015 }
1016 Ok(times[0])
1017 }
1018
1019 fn running(&self) -> bool {
1020 unsafe { WaitForSingleObject(self.0, 0) == WAIT_TIMEOUT }
1022 }
1023
1024 fn image(&self) -> io::Result<PathBuf> {
1026 let mut buffer = vec![0u16; LONGEST_PATH];
1027 let mut length = buffer.len() as u32;
1028 if unsafe { QueryFullProcessImageNameW(self.0, 0, buffer.as_mut_ptr(), &mut length) }
1031 == 0
1032 {
1033 return Err(io::Error::last_os_error());
1034 }
1035 Ok(PathBuf::from(OsString::from_wide(
1036 &buffer[..length as usize],
1037 )))
1038 }
1039 }
1040
1041 fn server_pid(pipe: RawHandle) -> io::Result<u32> {
1042 let mut pid = 0u32;
1043 if unsafe { GetNamedPipeServerProcessId(pipe, &mut pid) } == 0 {
1045 return Err(io::Error::last_os_error());
1046 }
1047 Ok(pid)
1048 }
1049
1050 pub(super) fn file_id(path: &Path) -> Option<(u32, u64)> {
1053 let file = std::fs::File::open(path).ok()?;
1054 let mut info = ByHandleFileInformation::default();
1055 if unsafe { GetFileInformationByHandle(file.as_raw_handle(), &mut info) } == 0 {
1057 return None;
1058 }
1059 Some((
1060 info.volume_serial,
1061 (u64::from(info.index_high) << 32) | u64::from(info.index_low),
1062 ))
1063 }
1064
1065 pub(super) fn same_executable(
1069 expected: &Path,
1070 found: &Path,
1071 found_id: Option<(u32, u64)>,
1072 ) -> bool {
1073 let Ok(expected) = std::fs::canonicalize(expected) else {
1074 return false;
1075 };
1076 let lower = |path: &Path| path.to_string_lossy().to_lowercase();
1077 if lower(&expected) == lower(found) {
1078 return true;
1079 }
1080 found_id.is_some() && file_id(&expected) == found_id
1081 }
1082
1083 fn plain(path: PathBuf) -> PathBuf {
1085 let text = path.to_string_lossy();
1086 match text.strip_prefix(r"\\?\UNC\") {
1087 Some(rest) => PathBuf::from(format!(r"\\{rest}")),
1088 None => match text.strip_prefix(r"\\?\") {
1089 Some(rest) => PathBuf::from(rest),
1090 None => path,
1091 },
1092 }
1093 }
1094
1095 pub(crate) fn verify_pipe_host(
1110 pipe: RawHandle,
1111 expect: &HostExpectation,
1112 ) -> Result<HostProcess, HostConnectError> {
1113 let refuse = |pid: Option<u32>, exe: Option<PathBuf>, reason: HostIdentityReason| {
1114 HostConnectError::Identity(HostIdentityError {
1115 expected: expect.executables.clone(),
1116 found_pid: pid,
1117 found_executable: exe,
1118 reason,
1119 })
1120 };
1121 if expect.executables.is_empty() {
1122 return Err(refuse(None, None, HostIdentityReason::NoExpectation));
1123 }
1124
1125 let first = match verify_pipe_server_is_current_user(pipe) {
1126 Ok(id) => id,
1127 Err(e) => {
1128 if e.peer_gone() {
1130 return Err(HostConnectError::Io(io::Error::from(e)));
1131 }
1132 let pid = match &e {
1133 PeerError::Mismatch(id) => id.pid,
1134 _ => None,
1135 };
1136 return Err(refuse(pid, None, HostIdentityReason::Query(e.to_string())));
1137 }
1138 };
1139 let Some(pid) = first.pid else {
1140 return Err(refuse(None, None, HostIdentityReason::PidUnavailable));
1141 };
1142 let unreadable = |what: &str, e: io::Error| {
1143 refuse(
1144 Some(pid),
1145 None,
1146 HostIdentityReason::Unreadable(format!("{what}: {e}")),
1147 )
1148 };
1149 let changed = |why: &str| {
1150 refuse(
1151 Some(pid),
1152 None,
1153 HostIdentityReason::HostChanged(why.to_string()),
1154 )
1155 };
1156
1157 let process = match Process::open(pid) {
1158 Ok(process) => process,
1159 Err(e) if e.raw_os_error() == Some(ERROR_INVALID_PARAMETER) => {
1161 return Err(changed("it exited before it could be opened"))
1162 }
1163 Err(e) => return Err(unreadable("open process", e)),
1164 };
1165 let before = process
1166 .created()
1167 .map_err(|e| unreadable("creation time", e))?;
1168 let image = process
1169 .image()
1170 .map_err(|e| unreadable("executable path", e))?;
1171 let after = process
1172 .created()
1173 .map_err(|e| unreadable("creation time", e))?;
1174 if before != after {
1175 return Err(changed(
1176 "its creation time differed before and after its executable path was read",
1177 ));
1178 }
1179 if !process.running() {
1180 return Err(changed("it exited while it was being identified"));
1181 }
1182 match server_pid(pipe) {
1183 Ok(again) if again == pid => {}
1184 _ => return Err(changed("the pipe no longer reports the same process id")),
1185 }
1186
1187 let found = std::fs::canonicalize(&image).unwrap_or(image);
1188 let id = file_id(&found);
1189 if !expect
1190 .executables
1191 .iter()
1192 .any(|expected| same_executable(expected, &found, id))
1193 {
1194 return Err(refuse(
1195 Some(pid),
1196 Some(plain(found)),
1197 HostIdentityReason::ExecutableMismatch,
1198 ));
1199 }
1200
1201 let host = HostProcess {
1202 pid,
1203 uid: None,
1204 sid: first.sid,
1205 executable: plain(found),
1206 audit_token: None,
1207 };
1208 if let Some(verifier) = &expect.verifier {
1209 if let Err(why) = verifier(&host) {
1210 return Err(refuse(
1211 Some(pid),
1212 Some(host.executable.clone()),
1213 HostIdentityReason::Refused(why),
1214 ));
1215 }
1216 }
1217 drop(process);
1219 Ok(host)
1220 }
1221
1222 #[cfg(test)]
1223 mod tests {
1224 use super::*;
1225
1226 #[test]
1227 fn the_same_file_matches_by_path_in_any_case_and_by_identity_through_a_hard_link() {
1228 let dir = tempfile::tempdir().unwrap();
1229 let real = dir.path().join("HostBin.exe");
1230 std::fs::write(&real, b"binary").unwrap();
1231 let found = std::fs::canonicalize(&real).unwrap();
1232 let id = file_id(&found);
1233 assert!(id.is_some());
1234
1235 assert!(same_executable(&real, &found, id));
1236 assert!(same_executable(&dir.path().join("hostbin.EXE"), &found, id));
1238 assert!(same_executable(
1239 &dir.path().join("hostbin.EXE"),
1240 &found,
1241 None
1242 ));
1243
1244 let hard = dir.path().join("hard.exe");
1245 std::fs::hard_link(&real, &hard).unwrap();
1246 assert!(same_executable(&hard, &found, id), "same file, other name");
1247 assert!(!same_executable(&hard, &found, None));
1249
1250 let copy = dir.path().join("copy.exe");
1252 std::fs::copy(&real, ©).unwrap();
1253 assert!(!same_executable(©, &found, id));
1254 assert!(!same_executable(
1255 &dir.path().join("missing.exe"),
1256 &found,
1257 id
1258 ));
1259 }
1260
1261 #[test]
1262 fn this_process_is_described_by_its_own_handle() {
1263 let process = Process::open(std::process::id()).unwrap();
1264 assert!(process.running());
1265 assert_eq!(process.created().unwrap(), process.created().unwrap());
1266 let image = std::fs::canonicalize(process.image().unwrap()).unwrap();
1267 let me = std::fs::canonicalize(std::env::current_exe().unwrap()).unwrap();
1268 assert!(same_executable(&me, &image, file_id(&image)));
1269 assert!(!plain(image).to_string_lossy().starts_with(r"\\?\"));
1270 }
1271
1272 #[test]
1273 fn a_process_id_nothing_holds_cannot_be_opened() {
1274 assert!(Process::open(0xFFFF_FFF3).is_err());
1276 }
1277 }
1278}
1279
1280#[cfg(test)]
1281mod tests {
1282 use super::*;
1283
1284 #[test]
1285 fn a_bare_file_name_is_one_component() {
1286 assert!(is_single_component(OsStr::new("mailright")));
1287 assert!(is_single_component(OsStr::new("mailright.exe")));
1288 assert!(!is_single_component(OsStr::new("")));
1289 assert!(!is_single_component(OsStr::new("..")));
1290 assert!(!is_single_component(OsStr::new("a/b")));
1291 assert!(!is_single_component(OsStr::new("/abs")));
1292 }
1293
1294 #[test]
1295 fn install_relative_paths_are_derived_from_where_the_engine_lives() {
1296 let engine = Path::new("/Applications/MailRight.app/Contents/Resources/bin/engine");
1297 assert_eq!(
1298 bundle_main(engine, OsStr::new("mailright")),
1299 Some(PathBuf::from(
1300 "/Applications/MailRight.app/Contents/MacOS/mailright"
1301 ))
1302 );
1303 assert_eq!(
1304 up_and_join(engine, 1, Path::new("app")),
1305 Some(PathBuf::from(
1306 "/Applications/MailRight.app/Contents/Resources/bin/app"
1307 ))
1308 );
1309 assert_eq!(
1310 up_and_join(engine, 3, Path::new("MacOS/mailright")),
1311 Some(PathBuf::from(
1312 "/Applications/MailRight.app/Contents/MacOS/mailright"
1313 ))
1314 );
1315 assert_eq!(
1317 bundle_main(Path::new("/opt/app/bin/engine"), OsStr::new("x")),
1318 None
1319 );
1320 assert_eq!(up_and_join(engine, 0, Path::new("x")), None);
1321 assert_eq!(up_and_join(engine, 1, Path::new("/etc/passwd")), None);
1322 #[cfg(windows)]
1324 for escape in [r"\Windows\System32\cmd.exe", "C:cmd.exe", r"C:\cmd.exe"] {
1325 assert_eq!(up_and_join(engine, 1, Path::new(escape)), None, "{escape}");
1326 }
1327 assert_eq!(up_and_join(Path::new("engine"), 5, Path::new("x")), None);
1328 }
1329
1330 #[test]
1331 fn helpers_resolve_against_the_running_test_binary() {
1332 let exe = std::fs::canonicalize(std::env::current_exe().unwrap()).unwrap();
1333 let sibling = HostExpectation::sibling_executable("host-binary").unwrap();
1334 assert_eq!(
1335 sibling.executables(),
1336 [exe.parent().unwrap().join("host-binary")]
1337 );
1338 assert!(HostExpectation::sibling_executable("a/b").is_err());
1339 assert!(HostExpectation::sibling_executable("..").is_err());
1340 assert!(HostExpectation::bundle_executable("x/y").is_err());
1341 let up2 = HostExpectation::relative_to_current_exe(2, "bin/host").unwrap();
1342 assert_eq!(
1343 up2.executables(),
1344 [exe.parent().unwrap().parent().unwrap().join("bin/host")]
1345 );
1346 assert!(HostExpectation::relative_to_current_exe(0, "x").is_err());
1347 }
1348
1349 #[test]
1350 fn the_error_says_what_was_expected_and_what_was_found() {
1351 let e = HostIdentityError {
1352 expected: vec![PathBuf::from("/a/app"), PathBuf::from("/b/app")],
1353 found_pid: Some(42),
1354 found_executable: Some(PathBuf::from("/tmp/evil")),
1355 reason: HostIdentityReason::ExecutableMismatch,
1356 };
1357 let text = e.to_string();
1358 assert!(
1359 text.contains("pid 42") && text.contains("/tmp/evil"),
1360 "{text}"
1361 );
1362 assert!(text.contains("/a/app or /b/app"), "{text}");
1363 let none = HostIdentityError {
1364 expected: Vec::new(),
1365 found_pid: None,
1366 found_executable: None,
1367 reason: HostIdentityReason::NoExpectation,
1368 };
1369 assert!(none.to_string().contains("none supplied"));
1370 }
1371
1372 #[test]
1373 fn a_refusal_collapses_to_a_non_retryable_wire_error() {
1374 let e = HostIdentityError {
1375 expected: vec![PathBuf::from("/a/app")],
1376 found_pid: Some(7),
1377 found_executable: None,
1378 reason: HostIdentityReason::PidUnavailable,
1379 };
1380 let wire: crate::ServiceError = e.clone().into();
1381 assert_eq!(wire.code, crate::ERROR_HOST_IDENTITY);
1382 assert!(!wire.retryable);
1383 assert!(wire.message.contains("pid 7"));
1384 let c: crate::ConnectError = e.into();
1385 assert_eq!(c.into_service_error(), wire);
1386 }
1387}