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