1use std::fmt;
13use std::io;
14
15#[derive(Debug, Clone, PartialEq, Eq, Default)]
17#[non_exhaustive]
18pub struct PeerIdentity {
19 pub uid: Option<u32>,
21 pub pid: Option<u32>,
23 pub sid: Option<String>,
25}
26
27#[derive(Debug)]
29#[non_exhaustive]
30pub enum PeerError {
31 Unsupported(&'static str),
33 Mismatch(PeerIdentity),
35 Query(io::Error),
37}
38
39impl fmt::Display for PeerError {
40 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41 match self {
42 PeerError::Unsupported(why) => write!(f, "peer authentication unsupported: {why}"),
43 PeerError::Mismatch(id) => {
44 write!(f, "peer is not the current user")?;
45 if let Some(uid) = id.uid {
46 write!(f, " (uid {uid})")?;
47 }
48 if let Some(pid) = id.pid {
49 write!(f, " (pid {pid})")?;
50 }
51 Ok(())
52 }
53 PeerError::Query(e) => write!(f, "could not identify peer: {e}"),
54 }
55 }
56}
57
58impl std::error::Error for PeerError {
59 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
60 match self {
61 PeerError::Query(e) => Some(e),
62 _ => None,
63 }
64 }
65}
66
67impl From<PeerError> for io::Error {
68 fn from(e: PeerError) -> Self {
69 let kind = match &e {
70 PeerError::Unsupported(_) => io::ErrorKind::Unsupported,
71 PeerError::Mismatch(_) => io::ErrorKind::PermissionDenied,
72 PeerError::Query(q) => q.kind(),
73 };
74 io::Error::new(kind, e.to_string())
75 }
76}
77
78impl PeerError {
79 pub(crate) fn peer_gone(&self) -> bool {
82 let PeerError::Query(e) = self else {
83 return false;
84 };
85 if matches!(
86 e.kind(),
87 io::ErrorKind::NotConnected
88 | io::ErrorKind::BrokenPipe
89 | io::ErrorKind::ConnectionReset
90 | io::ErrorKind::UnexpectedEof
91 ) {
92 return true;
93 }
94 cfg!(windows) && matches!(e.raw_os_error(), Some(109 | 232 | 233))
96 }
97}
98
99#[cfg(any(windows, test))]
102pub(crate) fn sddl_for_sid(sid: &str) -> String {
103 format!("D:P(A;;GA;;;{sid})")
104}
105
106#[cfg(unix)]
111mod unix {
112 use super::*;
113 use std::os::fd::AsRawFd;
114 use std::os::unix::net::UnixStream;
115
116 extern "C" {
117 fn geteuid() -> u32;
118 }
119
120 #[cfg(any(
121 target_os = "macos",
122 target_os = "ios",
123 target_os = "freebsd",
124 target_os = "openbsd",
125 target_os = "netbsd",
126 target_os = "dragonfly"
127 ))]
128 extern "C" {
129 fn getpeereid(fd: i32, euid: *mut u32, egid: *mut u32) -> i32;
130 }
131
132 #[cfg(all(
136 any(target_os = "linux", target_os = "android"),
137 not(any(
138 target_arch = "mips",
139 target_arch = "mips32r6",
140 target_arch = "mips64",
141 target_arch = "mips64r6",
142 target_arch = "powerpc",
143 target_arch = "powerpc64",
144 target_arch = "sparc",
145 target_arch = "sparc64"
146 ))
147 ))]
148 mod linux {
149 pub const SOL_SOCKET: i32 = 1;
150 pub const SO_PEERCRED: i32 = 17;
151 #[repr(C)]
152 #[derive(Default)]
153 pub struct Ucred {
154 pub pid: i32,
155 pub uid: u32,
156 pub gid: u32,
157 }
158 extern "C" {
159 pub fn getsockopt(
160 fd: i32,
161 level: i32,
162 name: i32,
163 val: *mut core::ffi::c_void,
164 len: *mut u32,
165 ) -> i32;
166 }
167 }
168
169 pub(crate) fn current_euid() -> u32 {
170 unsafe { geteuid() }
171 }
172
173 #[cfg(any(
174 target_os = "macos",
175 target_os = "ios",
176 target_os = "freebsd",
177 target_os = "openbsd",
178 target_os = "netbsd",
179 target_os = "dragonfly"
180 ))]
181 fn peer_identity(s: &UnixStream) -> Result<PeerIdentity, PeerError> {
182 let (mut uid, mut gid) = (u32::MAX, u32::MAX);
183 let rc = unsafe { getpeereid(s.as_raw_fd(), &mut uid, &mut gid) };
185 if rc != 0 {
186 return Err(PeerError::Query(io::Error::last_os_error()));
187 }
188 Ok(PeerIdentity {
189 uid: Some(uid),
190 pid: peer_pid(s),
191 sid: None,
192 })
193 }
194
195 #[cfg(any(target_os = "macos", target_os = "ios"))]
198 fn peer_pid(s: &UnixStream) -> Option<u32> {
199 const SOL_LOCAL: i32 = 0;
200 const LOCAL_PEERPID: i32 = 0x002;
201 extern "C" {
202 fn getsockopt(
203 fd: i32,
204 level: i32,
205 name: i32,
206 val: *mut core::ffi::c_void,
207 len: *mut u32,
208 ) -> i32;
209 }
210 let mut pid: i32 = 0;
211 let mut len = std::mem::size_of::<i32>() as u32;
212 let rc = unsafe {
215 getsockopt(
216 s.as_raw_fd(),
217 SOL_LOCAL,
218 LOCAL_PEERPID,
219 (&mut pid as *mut i32).cast(),
220 &mut len,
221 )
222 };
223 if rc != 0 {
224 return None;
225 }
226 u32::try_from(pid).ok().filter(|p| *p != 0)
227 }
228
229 #[cfg(any(
231 target_os = "freebsd",
232 target_os = "openbsd",
233 target_os = "netbsd",
234 target_os = "dragonfly"
235 ))]
236 fn peer_pid(_s: &UnixStream) -> Option<u32> {
237 None
238 }
239
240 #[cfg(all(
241 any(target_os = "linux", target_os = "android"),
242 not(any(
243 target_arch = "mips",
244 target_arch = "mips32r6",
245 target_arch = "mips64",
246 target_arch = "mips64r6",
247 target_arch = "powerpc",
248 target_arch = "powerpc64",
249 target_arch = "sparc",
250 target_arch = "sparc64"
251 ))
252 ))]
253 fn peer_identity(s: &UnixStream) -> Result<PeerIdentity, PeerError> {
254 let mut cred = linux::Ucred::default();
255 let mut len = std::mem::size_of::<linux::Ucred>() as u32;
256 let rc = unsafe {
258 linux::getsockopt(
259 s.as_raw_fd(),
260 linux::SOL_SOCKET,
261 linux::SO_PEERCRED,
262 &mut cred as *mut linux::Ucred as *mut core::ffi::c_void,
263 &mut len,
264 )
265 };
266 if rc != 0 {
267 return Err(PeerError::Query(io::Error::last_os_error()));
268 }
269 if (len as usize) < std::mem::size_of::<linux::Ucred>() {
270 return Err(PeerError::Query(io::Error::new(
271 io::ErrorKind::InvalidData,
272 "short SO_PEERCRED reply",
273 )));
274 }
275 Ok(PeerIdentity {
276 uid: Some(cred.uid),
277 pid: u32::try_from(cred.pid).ok().filter(|p| *p != 0),
278 sid: None,
279 })
280 }
281
282 #[cfg(not(any(
283 target_os = "macos",
284 target_os = "ios",
285 target_os = "freebsd",
286 target_os = "openbsd",
287 target_os = "netbsd",
288 target_os = "dragonfly",
289 all(
290 any(target_os = "linux", target_os = "android"),
291 not(any(
292 target_arch = "mips",
293 target_arch = "mips32r6",
294 target_arch = "mips64",
295 target_arch = "mips64r6",
296 target_arch = "powerpc",
297 target_arch = "powerpc64",
298 target_arch = "sparc",
299 target_arch = "sparc64"
300 ))
301 )
302 )))]
303 fn peer_identity(_s: &UnixStream) -> Result<PeerIdentity, PeerError> {
304 Err(PeerError::Unsupported(
305 "no peer-credential query is implemented for this Unix platform",
306 ))
307 }
308
309 pub(crate) fn accept_uid(id: PeerIdentity, me: u32) -> Result<PeerIdentity, PeerError> {
311 if id.uid == Some(me) {
312 Ok(id)
313 } else {
314 Err(PeerError::Mismatch(id))
315 }
316 }
317
318 pub fn verify_peer_is_current_user(stream: &UnixStream) -> Result<PeerIdentity, PeerError> {
328 accept_uid(peer_identity(stream)?, current_euid())
329 }
330}
331
332#[cfg(unix)]
333pub(crate) use unix::current_euid;
334#[cfg(unix)]
335pub use unix::verify_peer_is_current_user;
336
337#[cfg(windows)]
342mod win {
343 use super::*;
344 use std::os::windows::io::RawHandle;
345 use windows::core::PWSTR;
346 use windows::Win32::Foundation::{CloseHandle, LocalFree, HANDLE, HLOCAL};
347 use windows::Win32::Security::Authorization::{
348 ConvertSidToStringSidW, ConvertStringSecurityDescriptorToSecurityDescriptorW,
349 SDDL_REVISION_1,
350 };
351 use windows::Win32::Security::{
352 GetTokenInformation, TokenUser, PSECURITY_DESCRIPTOR, PSID, SECURITY_ATTRIBUTES,
353 TOKEN_QUERY, TOKEN_USER,
354 };
355 use windows::Win32::System::Pipes::{GetNamedPipeClientProcessId, GetNamedPipeServerProcessId};
356 use windows::Win32::System::Threading::{
357 GetCurrentProcess, OpenProcess, OpenProcessToken, PROCESS_QUERY_LIMITED_INFORMATION,
358 };
359
360 fn win_err(e: windows::core::Error) -> io::Error {
362 io::Error::from_raw_os_error((e.code().0 as u32 & 0xFFFF) as i32)
363 }
364
365 struct Owned(HANDLE);
366 impl Drop for Owned {
367 fn drop(&mut self) {
368 unsafe {
369 let _ = CloseHandle(self.0);
370 }
371 }
372 }
373
374 fn sid_to_string(sid: PSID) -> io::Result<String> {
375 unsafe {
376 let mut p = PWSTR::null();
377 ConvertSidToStringSidW(sid, &mut p).map_err(win_err)?;
378 let s = p.to_string().map_err(io::Error::other);
379 let _ = LocalFree(Some(HLOCAL(p.0 as *mut _)));
380 s
381 }
382 }
383
384 fn token_user_sid(tok: HANDLE) -> io::Result<String> {
386 unsafe {
387 let mut need = 0u32;
388 let _ = GetTokenInformation(tok, TokenUser, None, 0, &mut need);
389 if need == 0 {
390 return Err(io::Error::other("token user size query returned 0"));
391 }
392 let mut buf = vec![0u64; (need as usize).div_ceil(8)];
394 GetTokenInformation(
395 tok,
396 TokenUser,
397 Some(buf.as_mut_ptr() as *mut _),
398 (buf.len() * 8) as u32,
399 &mut need,
400 )
401 .map_err(win_err)?;
402 let user = &*(buf.as_ptr() as *const TOKEN_USER);
403 sid_to_string(user.User.Sid)
404 }
405 }
406
407 pub(crate) fn current_user_sid_string() -> io::Result<String> {
409 unsafe {
410 let mut tok = HANDLE::default();
411 OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut tok).map_err(win_err)?;
412 let tok = Owned(tok);
413 token_user_sid(tok.0)
414 }
415 }
416
417 fn process_user_sid_string(pid: u32) -> io::Result<String> {
420 unsafe {
421 let proc =
422 Owned(OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, pid).map_err(win_err)?);
423 let mut tok = HANDLE::default();
424 OpenProcessToken(proc.0, TOKEN_QUERY, &mut tok).map_err(win_err)?;
425 let tok = Owned(tok);
426 token_user_sid(tok.0)
427 }
428 }
429
430 fn verify_pid(pid: u32) -> Result<PeerIdentity, PeerError> {
432 if pid == 0 {
433 return Err(PeerError::Query(io::Error::other("peer process id is 0")));
434 }
435 let me = current_user_sid_string().map_err(PeerError::Query)?;
436 let theirs = process_user_sid_string(pid).map_err(PeerError::Query)?;
437 let id = PeerIdentity {
438 uid: None,
439 pid: Some(pid),
440 sid: Some(theirs.clone()),
441 };
442 if theirs.eq_ignore_ascii_case(&me) {
445 Ok(id)
446 } else {
447 Err(PeerError::Mismatch(id))
448 }
449 }
450
451 pub fn verify_pipe_peer_is_current_user(handle: RawHandle) -> Result<PeerIdentity, PeerError> {
463 let mut pid = 0u32;
464 unsafe { GetNamedPipeClientProcessId(HANDLE(handle), &mut pid) }
466 .map_err(|e| PeerError::Query(win_err(e)))?;
467 verify_pid(pid)
468 }
469
470 pub fn verify_pipe_server_is_current_user(
476 handle: RawHandle,
477 ) -> Result<PeerIdentity, PeerError> {
478 let mut pid = 0u32;
479 unsafe { GetNamedPipeServerProcessId(HANDLE(handle), &mut pid) }
481 .map_err(|e| PeerError::Query(win_err(e)))?;
482 verify_pid(pid)
483 }
484
485 pub struct OwnerOnlySecurityAttributes {
502 attrs: Box<SECURITY_ATTRIBUTES>,
503 sd: PSECURITY_DESCRIPTOR,
504 sddl: String,
505 }
506
507 unsafe impl Send for OwnerOnlySecurityAttributes {}
510 unsafe impl Sync for OwnerOnlySecurityAttributes {}
511
512 impl OwnerOnlySecurityAttributes {
513 pub fn as_ptr(&self) -> *const core::ffi::c_void {
518 &*self.attrs as *const SECURITY_ATTRIBUTES as *const core::ffi::c_void
519 }
520
521 pub fn sddl(&self) -> &str {
523 &self.sddl
524 }
525
526 pub(crate) fn raw(&self) -> &SECURITY_ATTRIBUTES {
527 &self.attrs
528 }
529 }
530
531 impl Drop for OwnerOnlySecurityAttributes {
532 fn drop(&mut self) {
533 unsafe {
534 let _ = LocalFree(Some(HLOCAL(self.sd.0)));
535 }
536 }
537 }
538
539 pub fn owner_only_security_attributes() -> io::Result<OwnerOnlySecurityAttributes> {
542 let sddl = sddl_for_sid(¤t_user_sid_string()?);
543 let wide: Vec<u16> = sddl.encode_utf16().chain(std::iter::once(0)).collect();
544 let mut sd = PSECURITY_DESCRIPTOR::default();
545 unsafe {
546 ConvertStringSecurityDescriptorToSecurityDescriptorW(
547 windows::core::PCWSTR(wide.as_ptr()),
548 SDDL_REVISION_1,
549 &mut sd,
550 None,
551 )
552 .map_err(win_err)?;
553 }
554 let attrs = Box::new(SECURITY_ATTRIBUTES {
555 nLength: std::mem::size_of::<SECURITY_ATTRIBUTES>() as u32,
556 lpSecurityDescriptor: sd.0,
557 bInheritHandle: false.into(),
558 });
559 Ok(OwnerOnlySecurityAttributes { attrs, sd, sddl })
560 }
561}
562
563#[cfg(all(windows, test))]
564pub(crate) use win::current_user_sid_string;
565#[cfg(windows)]
566pub use win::{
567 owner_only_security_attributes, verify_pipe_peer_is_current_user,
568 verify_pipe_server_is_current_user, OwnerOnlySecurityAttributes,
569};
570
571#[cfg(test)]
572mod tests {
573 use super::*;
574
575 #[test]
576 fn sddl_is_built_from_the_given_sid_with_a_protected_single_ace_dacl() {
577 assert_eq!(
578 sddl_for_sid("S-1-5-21-1-2-3-1001"),
579 "D:P(A;;GA;;;S-1-5-21-1-2-3-1001)"
580 );
581 }
582
583 #[test]
584 fn peer_error_maps_to_typed_io_kinds() {
585 let k = |e: PeerError| io::Error::from(e).kind();
586 assert_eq!(k(PeerError::Unsupported("x")), io::ErrorKind::Unsupported);
587 assert_eq!(
588 k(PeerError::Mismatch(PeerIdentity::default())),
589 io::ErrorKind::PermissionDenied
590 );
591 }
592
593 #[test]
594 fn a_vanished_peer_is_not_an_attack_but_a_mismatch_always_is() {
595 assert!(PeerError::Query(io::Error::from(io::ErrorKind::NotConnected)).peer_gone());
596 assert!(!PeerError::Query(io::Error::from(io::ErrorKind::PermissionDenied)).peer_gone());
597 assert!(!PeerError::Mismatch(PeerIdentity::default()).peer_gone());
598 assert!(!PeerError::Unsupported("x").peer_gone());
599 }
600
601 #[cfg(unix)]
602 mod unix_tests {
603 use super::*;
604 use std::os::unix::net::UnixStream;
605
606 #[test]
607 fn same_user_socketpair_is_accepted_with_our_uid() {
608 let (a, b) = UnixStream::pair().unwrap();
609 for s in [&a, &b] {
610 match verify_peer_is_current_user(s) {
611 Ok(id) => assert_eq!(id.uid, Some(current_euid())),
612 Err(PeerError::Unsupported(_)) => {}
614 Err(e) => panic!("same-user socketpair rejected: {e}"),
615 }
616 }
617 }
618
619 #[test]
620 fn a_different_uid_is_a_mismatch_and_an_unknown_uid_fails_closed() {
621 let me = current_euid();
622 let other = PeerIdentity {
623 uid: Some(me.wrapping_add(1)),
624 ..Default::default()
625 };
626 assert!(matches!(
627 super::super::unix::accept_uid(other, me),
628 Err(PeerError::Mismatch(id)) if id.uid == Some(me.wrapping_add(1))
629 ));
630 assert!(matches!(
631 super::super::unix::accept_uid(PeerIdentity::default(), me),
632 Err(PeerError::Mismatch(_))
633 ));
634 assert!(super::super::unix::accept_uid(
635 PeerIdentity {
636 uid: Some(me),
637 ..Default::default()
638 },
639 me
640 )
641 .is_ok());
642 }
643
644 #[test]
650 #[ignore = "needs a second OS account; see comment"]
651 fn cross_user_socket_is_rejected() {
652 let path = std::env::var("RIGHTKIT_SERVICE_FOREIGN_SOCKET")
653 .expect("set RIGHTKIT_SERVICE_FOREIGN_SOCKET to a socket served by another user");
654 let s = UnixStream::connect(path).unwrap();
655 assert!(matches!(
656 verify_peer_is_current_user(&s),
657 Err(PeerError::Mismatch(_))
658 ));
659 }
660 }
661
662 #[cfg(windows)]
663 mod windows_tests {
664 use super::*;
665
666 #[test]
667 fn owner_only_attributes_use_the_real_current_sid() {
668 let sid = current_user_sid_string().unwrap();
669 assert!(sid.starts_with("S-1-"), "{sid}");
670 let a = owner_only_security_attributes().unwrap();
671 assert_eq!(a.sddl(), format!("D:P(A;;GA;;;{sid})"));
672 assert!(!a.as_ptr().is_null());
673 assert!(!a.raw().lpSecurityDescriptor.is_null());
674 assert!(!a.raw().bInheritHandle.as_bool());
675 }
676 }
677}