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 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
240fn is_single_component(name: &OsStr) -> bool {
242 let path = Path::new(name);
243 path.file_name() == Some(name) && path.components().count() == 1
244}
245
246fn 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
263fn 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 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 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 pub fn or_executable(mut self, path: impl Into<PathBuf>) -> Self {
296 self.executables.push(path.into());
297 self
298 }
299
300 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 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 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 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 pub fn executables(&self) -> &[PathBuf] {
367 &self.executables
368 }
369
370 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
384pub(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
421pub(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#[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 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 pub struct ExeInfo {
476 pub path: PathBuf,
477 pub id: Option<FileId>,
480 }
481
482 #[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 #[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 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 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 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 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 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 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 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 #[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 #[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 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 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 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 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 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 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 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 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 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 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 pub fn audit_token(_stream: &UnixStream) -> Option<[u32; 8]> {
992 None
993 }
994 }
995
996 #[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 #[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#[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 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 #[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 struct Process(RawHandle);
1092
1093 impl Drop for Process {
1094 fn drop(&mut self) {
1095 unsafe { CloseHandle(self.0) };
1097 }
1098 }
1099
1100 impl Process {
1101 fn open(pid: u32) -> io::Result<Process> {
1102 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 fn created(&self) -> io::Result<FileTime> {
1113 let mut times = [FileTime::default(); 4];
1114 let [creation, exit, kernel, user] = &mut times;
1115 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 unsafe { WaitForSingleObject(self.0, 0) == WAIT_TIMEOUT }
1125 }
1126
1127 fn image(&self) -> io::Result<PathBuf> {
1129 let mut buffer = vec![0u16; LONGEST_PATH];
1130 let mut length = buffer.len() as u32;
1131 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 if unsafe { GetNamedPipeServerProcessId(pipe, &mut pid) } == 0 {
1148 return Err(io::Error::last_os_error());
1149 }
1150 Ok(pid)
1151 }
1152
1153 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 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 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 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 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 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 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 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 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 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 drop(process);
1446 Ok(host)
1447 }
1448}