1#[cfg(unix)]
9use std::path::Path;
10use std::path::PathBuf;
11
12use super::{
13 HandoffAttemptDecision, HandoffAttemptFailure, HandoffFallbackDecision, HandoffFallbackReason,
14 HandoffToken,
15};
16
17pub const SCM_RIGHTS_TRANSPORT_SUPPORTED: bool = cfg!(unix);
19
20#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
22pub struct UnixFileDescriptor(i32);
23
24impl UnixFileDescriptor {
25 pub fn new(raw_fd: i32) -> Self {
27 Self(raw_fd)
28 }
29
30 pub fn raw(self) -> i32 {
32 self.0
33 }
34}
35
36#[derive(Clone, Debug, PartialEq, Eq)]
38pub struct UnixHandoffSocket {
39 pub path: PathBuf,
41}
42
43impl UnixHandoffSocket {
44 pub fn new(path: impl Into<PathBuf>) -> Self {
46 Self { path: path.into() }
47 }
48}
49
50#[derive(Clone, Debug, PartialEq, Eq)]
52pub struct ScmRightsAttempt {
53 pub fd: UnixFileDescriptor,
55 pub backend_socket: UnixHandoffSocket,
57 pub handoff_token: HandoffToken,
59}
60
61impl ScmRightsAttempt {
62 pub fn new(
64 fd: UnixFileDescriptor,
65 backend_socket: UnixHandoffSocket,
66 handoff_token: HandoffToken,
67 ) -> Self {
68 Self {
69 fd,
70 backend_socket,
71 handoff_token,
72 }
73 }
74}
75
76#[derive(Clone, Debug, PartialEq, Eq)]
78pub struct ScmRightsSuccess {
79 pub sent_fd: UnixFileDescriptor,
81 pub backend_socket: UnixHandoffSocket,
83 pub handoff_token: HandoffToken,
85}
86
87impl ScmRightsSuccess {
88 pub fn new(
90 sent_fd: UnixFileDescriptor,
91 backend_socket: UnixHandoffSocket,
92 handoff_token: HandoffToken,
93 ) -> Self {
94 Self {
95 sent_fd,
96 backend_socket,
97 handoff_token,
98 }
99 }
100}
101
102pub type ScmRightsResult = Result<ScmRightsSuccess, ScmRightsError>;
104
105pub fn try_send_scm_rights(attempt: &ScmRightsAttempt) -> ScmRightsResult {
111 platform_try_send_scm_rights(attempt)
112}
113
114#[cfg(unix)]
123pub fn try_send_scm_rights_over(
124 socket_fd: std::os::fd::RawFd,
125 attempt: &ScmRightsAttempt,
126) -> ScmRightsResult {
127 send_fd_with_token(
128 socket_fd,
129 attempt.fd.raw(),
130 attempt.handoff_token.as_bytes(),
131 &attempt.backend_socket.path,
132 )?;
133 Ok(ScmRightsSuccess::new(
134 attempt.fd,
135 attempt.backend_socket.clone(),
136 attempt.handoff_token,
137 ))
138}
139
140#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
142pub enum ScmRightsError {
143 #[error("SCM_RIGHTS handoff transport is unsupported on this platform")]
145 UnsupportedPlatform,
146 #[error("permission denied passing fd {fd} to backend handoff socket {socket}")]
148 PermissionDenied {
149 fd: i32,
151 socket: PathBuf,
153 },
154 #[error("backend handoff socket is unavailable: {socket}")]
156 BackendSocketUnavailable {
157 socket: PathBuf,
159 },
160 #[error("SCM_RIGHTS send would block for backend handoff socket {socket}")]
162 WouldBlock {
163 socket: PathBuf,
165 },
166 #[error("SCM_RIGHTS send failed for fd {fd} to backend handoff socket {socket}")]
168 SendFailed {
169 fd: i32,
171 socket: PathBuf,
173 raw_os_error: Option<i32>,
175 },
176 #[error(
178 "SCM_RIGHTS send was partial ({sent_bytes}/{expected_bytes} bytes) for fd {fd} to backend handoff socket {socket}"
179 )]
180 PartialSend {
181 fd: i32,
183 socket: PathBuf,
185 sent_bytes: usize,
187 expected_bytes: usize,
189 },
190 #[error("backend handoff socket {socket} did not acknowledge passed fd")]
192 BackendAckTimeout {
193 socket: PathBuf,
195 },
196}
197
198#[cfg(unix)]
199fn platform_try_send_scm_rights(attempt: &ScmRightsAttempt) -> ScmRightsResult {
200 use std::os::fd::AsRawFd;
201 use std::os::unix::net::UnixStream;
202
203 let stream = UnixStream::connect(&attempt.backend_socket.path)
204 .map_err(|err| socket_connect_error(&attempt.backend_socket.path, err))?;
205 stream
206 .set_nonblocking(true)
207 .map_err(|err| socket_connect_error(&attempt.backend_socket.path, err))?;
208
209 send_fd_with_token(
210 stream.as_raw_fd(),
211 attempt.fd.raw(),
212 attempt.handoff_token.as_bytes(),
213 &attempt.backend_socket.path,
214 )?;
215
216 Ok(ScmRightsSuccess::new(
217 attempt.fd,
218 attempt.backend_socket.clone(),
219 attempt.handoff_token,
220 ))
221}
222
223#[cfg(not(unix))]
224fn platform_try_send_scm_rights(_attempt: &ScmRightsAttempt) -> ScmRightsResult {
225 Err(ScmRightsError::UnsupportedPlatform)
226}
227
228#[cfg(unix)]
229fn send_fd_with_token(
230 socket_fd: std::os::fd::RawFd,
231 sent_fd: std::os::fd::RawFd,
232 token: &[u8; 16],
233 socket_path: &Path,
234) -> Result<(), ScmRightsError> {
235 let mut token_payload = *token;
236 let mut iov = libc::iovec {
237 iov_base: token_payload.as_mut_ptr().cast(),
238 iov_len: token_payload.len(),
239 };
240 let mut control = vec![0_u8; cmsg_space::<libc::c_int>()];
241 let mut message = unsafe { std::mem::zeroed::<libc::msghdr>() };
242 message.msg_iov = &mut iov;
243 message.msg_iovlen = 1;
244 message.msg_control = control.as_mut_ptr().cast();
245 message.msg_controllen = control.len() as _;
246
247 unsafe {
248 let header = libc::CMSG_FIRSTHDR(&message);
249 if header.is_null() {
250 return Err(ScmRightsError::SendFailed {
251 fd: sent_fd,
252 socket: socket_path.to_path_buf(),
253 raw_os_error: None,
254 });
255 }
256
257 (*header).cmsg_level = libc::SOL_SOCKET;
258 (*header).cmsg_type = libc::SCM_RIGHTS;
259 (*header).cmsg_len = cmsg_len::<libc::c_int>() as _;
260 *libc::CMSG_DATA(header).cast::<libc::c_int>() = sent_fd;
261 }
262
263 let sent = unsafe { libc::sendmsg(socket_fd, &message, sendmsg_flags()) };
264 if sent < 0 {
265 return Err(sendmsg_error(
266 sent_fd,
267 socket_path,
268 std::io::Error::last_os_error(),
269 ));
270 }
271 if sent as usize != token_payload.len() {
272 return Err(ScmRightsError::PartialSend {
273 fd: sent_fd,
274 socket: socket_path.to_path_buf(),
275 sent_bytes: sent as usize,
276 expected_bytes: token_payload.len(),
277 });
278 }
279
280 Ok(())
281}
282
283#[cfg(unix)]
284fn cmsg_space<T>() -> usize {
285 unsafe { libc::CMSG_SPACE(std::mem::size_of::<T>() as _) as usize }
286}
287
288#[cfg(unix)]
289fn cmsg_len<T>() -> usize {
290 unsafe { libc::CMSG_LEN(std::mem::size_of::<T>() as _) as usize }
291}
292
293#[cfg(all(unix, any(target_os = "android", target_os = "linux")))]
294fn sendmsg_flags() -> libc::c_int {
295 combine_sendmsg_flags(libc::MSG_DONTWAIT, Some(libc::MSG_NOSIGNAL))
296}
297
298#[cfg(all(unix, not(any(target_os = "android", target_os = "linux"))))]
299fn sendmsg_flags() -> libc::c_int {
300 combine_sendmsg_flags(libc::MSG_DONTWAIT, None)
301}
302
303#[cfg(any(test, unix))]
304fn combine_sendmsg_flags(dontwait: libc::c_int, nosignal: Option<libc::c_int>) -> libc::c_int {
305 dontwait | nosignal.unwrap_or(0)
306}
307
308#[cfg(unix)]
309fn socket_connect_error(socket: &Path, error: std::io::Error) -> ScmRightsError {
310 match error.kind() {
311 std::io::ErrorKind::PermissionDenied => ScmRightsError::PermissionDenied {
312 fd: -1,
313 socket: socket.to_path_buf(),
314 },
315 std::io::ErrorKind::WouldBlock => ScmRightsError::WouldBlock {
316 socket: socket.to_path_buf(),
317 },
318 _ => ScmRightsError::BackendSocketUnavailable {
319 socket: socket.to_path_buf(),
320 },
321 }
322}
323
324#[cfg(unix)]
325fn sendmsg_error(fd: std::os::fd::RawFd, socket: &Path, error: std::io::Error) -> ScmRightsError {
326 let raw_os_error = error.raw_os_error();
331 let transient_backpressure = raw_os_error == Some(libc::ENOBUFS)
332 || cfg!(target_os = "macos") && raw_os_error == Some(libc::EMSGSIZE);
333 if transient_backpressure {
334 return ScmRightsError::WouldBlock {
335 socket: socket.to_path_buf(),
336 };
337 }
338 match error.kind() {
339 std::io::ErrorKind::PermissionDenied => ScmRightsError::PermissionDenied {
340 fd,
341 socket: socket.to_path_buf(),
342 },
343 std::io::ErrorKind::WouldBlock => ScmRightsError::WouldBlock {
344 socket: socket.to_path_buf(),
345 },
346 std::io::ErrorKind::ConnectionRefused
347 | std::io::ErrorKind::ConnectionReset
348 | std::io::ErrorKind::BrokenPipe
349 | std::io::ErrorKind::NotConnected => ScmRightsError::BackendSocketUnavailable {
350 socket: socket.to_path_buf(),
351 },
352 _ => ScmRightsError::SendFailed {
353 fd,
354 socket: socket.to_path_buf(),
355 raw_os_error: error.raw_os_error(),
356 },
357 }
358}
359
360impl ScmRightsError {
361 pub fn attempt_failure(&self) -> Option<HandoffAttemptFailure> {
363 match self {
364 Self::UnsupportedPlatform => None,
365 Self::PermissionDenied { .. } => Some(HandoffAttemptFailure::PermissionDenied),
366 Self::BackendSocketUnavailable { .. }
367 | Self::WouldBlock { .. }
368 | Self::SendFailed { .. }
369 | Self::PartialSend { .. }
370 | Self::BackendAckTimeout { .. } => Some(HandoffAttemptFailure::BackendAckTimeout),
371 }
372 }
373
374 pub fn fallback_reason(&self) -> HandoffFallbackReason {
376 match self.attempt_failure() {
377 Some(failure) => failure.into(),
378 None => HandoffFallbackReason::ServicePolicyDisabled,
379 }
380 }
381
382 pub fn fallback_decision(&self) -> HandoffFallbackDecision {
384 HandoffFallbackDecision::new(self.fallback_reason())
385 }
386
387 pub fn fallback_attempt_decision(&self) -> HandoffAttemptDecision {
389 HandoffAttemptDecision::FallbackToReconnect(self.fallback_decision())
390 }
391
392 pub fn is_fallback_safe(&self) -> bool {
394 let fallback = self.fallback_decision();
395 fallback.uses_backend_reconnect() && !fallback.sends_client_error()
396 }
397
398 pub fn fd_may_have_reached_backend(&self) -> bool {
404 matches!(self, Self::PartialSend { sent_bytes, .. } if *sent_bytes > 0)
405 }
406}
407
408#[cfg(test)]
409mod platform_neutral_tests {
410 use super::{combine_sendmsg_flags, ScmRightsError};
411
412 #[test]
413 fn sendmsg_flags_always_include_per_call_nonblocking() {
414 const MOCK_DONTWAIT: i32 = 0b0010;
415 const MOCK_NOSIGNAL: i32 = 0b1000;
416
417 assert_ne!(
418 combine_sendmsg_flags(MOCK_DONTWAIT, Some(MOCK_NOSIGNAL)) & MOCK_DONTWAIT,
419 0
420 );
421 assert_ne!(
422 combine_sendmsg_flags(MOCK_DONTWAIT, None) & MOCK_DONTWAIT,
423 0
424 );
425 }
426
427 #[test]
428 fn positive_partial_send_tracks_indeterminate_fd_delivery() {
429 let error = ScmRightsError::PartialSend {
430 fd: 7,
431 socket: "handoff".into(),
432 sent_bytes: 1,
433 expected_bytes: 16,
434 };
435
436 assert!(error.fd_may_have_reached_backend());
437 assert!(error.is_fallback_safe());
438 }
439}
440
441#[cfg(all(test, unix))]
442mod tests {
443 use std::fs::File;
444 use std::io::Write;
445 use std::os::fd::{AsRawFd, RawFd};
446 use std::os::unix::net::{UnixListener, UnixStream};
447 use std::sync::mpsc;
448 use std::thread;
449 use std::time::Duration;
450
451 use super::*;
452
453 #[test]
454 fn send_scm_rights_to_backend_socket_transfers_fd_and_token() {
455 let dir = tempfile::tempdir().unwrap();
456 let socket_path = dir.path().join("handoff.sock");
457 let listener = UnixListener::bind(&socket_path).unwrap();
458 let expected_token = HandoffToken::from_bytes([0x41; 16]);
459 let receiver = thread::spawn(move || {
460 let (stream, _) = listener.accept().unwrap();
461 recv_fd_and_token(stream)
462 });
463 let file = File::open("/dev/null").unwrap();
464 let attempt = ScmRightsAttempt::new(
465 UnixFileDescriptor::new(file.as_raw_fd()),
466 UnixHandoffSocket::new(socket_path),
467 expected_token,
468 );
469
470 let success = try_send_scm_rights(&attempt).unwrap();
471 let (received_fd, received_token) = receiver.join().unwrap();
472
473 assert_eq!(success.sent_fd, attempt.fd);
474 assert_eq!(success.handoff_token, expected_token);
475 assert_eq!(received_token, expected_token);
476 assert_ne!(received_fd, file.as_raw_fd());
477
478 unsafe {
479 libc::close(received_fd);
480 }
481 }
482
483 #[test]
484 fn missing_backend_socket_maps_to_fallback_safe_error() {
485 let dir = tempfile::tempdir().unwrap();
486 let socket = UnixHandoffSocket::new(dir.path().join("missing.sock"));
487 let file = File::open("/dev/null").unwrap();
488 let attempt = ScmRightsAttempt::new(
489 UnixFileDescriptor::new(file.as_raw_fd()),
490 socket.clone(),
491 HandoffToken::from_bytes([0x42; 16]),
492 );
493
494 let err = try_send_scm_rights(&attempt).unwrap_err();
495
496 assert!(matches!(
497 err,
498 ScmRightsError::BackendSocketUnavailable { socket: ref path }
499 if path == &socket.path
500 ));
501 assert!(err.is_fallback_safe());
502 }
503
504 #[test]
505 fn ancillary_queue_enobufs_maps_to_silent_would_block() {
506 let socket = Path::new("saturated-handoff");
507 let error = sendmsg_error(
508 7,
509 socket,
510 std::io::Error::from_raw_os_error(libc::ENOBUFS),
511 );
512
513 assert!(matches!(
514 error,
515 ScmRightsError::WouldBlock { socket: ref path } if path == socket
516 ));
517 assert!(error.is_fallback_safe());
518 assert!(!error.fallback_decision().sends_client_error());
519 }
520
521 #[cfg(target_os = "macos")]
522 #[test]
523 fn saturated_ancillary_queue_emsgsize_maps_to_silent_would_block() {
524 let socket = Path::new("saturated-handoff");
525 let error = sendmsg_error(
526 7,
527 socket,
528 std::io::Error::from_raw_os_error(libc::EMSGSIZE),
529 );
530
531 assert!(matches!(
532 error,
533 ScmRightsError::WouldBlock { socket: ref path } if path == socket
534 ));
535 assert!(error.is_fallback_safe());
536 assert!(!error.fallback_decision().sends_client_error());
537 }
538
539 #[test]
540 fn send_scm_rights_over_connected_socket_transfers_fd_and_token() {
541 let (sender, receiver) = UnixStream::pair().unwrap();
542 let file = File::open("/dev/null").unwrap();
543 let token = HandoffToken::from_bytes([0x43; 16]);
544 let socket = UnixHandoffSocket::new("connected-handoff");
545 let attempt = ScmRightsAttempt::new(
546 UnixFileDescriptor::new(file.as_raw_fd()),
547 socket,
548 token,
549 );
550
551 let success = try_send_scm_rights_over(sender.as_raw_fd(), &attempt).unwrap();
552 let (received_fd, received_token) = recv_fd_and_token(receiver);
553
554 assert_eq!(success.sent_fd, attempt.fd);
555 assert_eq!(received_token, token);
556 unsafe {
557 libc::close(received_fd);
558 }
559 }
560
561 #[test]
562 fn saturated_blocking_handoff_socket_returns_silent_would_block_promptly() {
563 let (mut sender, receiver) = UnixStream::pair().unwrap();
564 sender.set_nonblocking(true).unwrap();
565 let fill = [0_u8; 64 * 1024];
566 loop {
567 match sender.write(&fill) {
568 Ok(_) => {}
569 Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => break,
570 Err(error) => panic!("fill handoff socket: {error}"),
571 }
572 }
573 sender.set_nonblocking(false).unwrap();
574
575 let file = File::open("/dev/null").unwrap();
576 let socket = UnixHandoffSocket::new("saturated-handoff");
577 let attempt = ScmRightsAttempt::new(
578 UnixFileDescriptor::new(file.as_raw_fd()),
579 socket.clone(),
580 HandoffToken::from_bytes([0x44; 16]),
581 );
582 let (done_tx, done_rx) = mpsc::channel();
583 let send_thread = thread::spawn(move || {
584 let result = try_send_scm_rights_over(sender.as_raw_fd(), &attempt);
585 done_tx.send(result).unwrap();
586 });
587
588 let result = match done_rx.recv_timeout(Duration::from_millis(500)) {
589 Ok(result) => result,
590 Err(error) => {
591 drop(receiver);
592 send_thread.join().unwrap();
593 panic!("SCM_RIGHTS send exceeded nonblocking deadline: {error}");
594 }
595 };
596 drop(receiver);
597 send_thread.join().unwrap();
598
599 let error = result.unwrap_err();
600 assert!(matches!(
601 error,
602 ScmRightsError::WouldBlock { socket: ref path } if path == &socket.path
603 ), "unexpected saturated send result: {error:?}");
604 assert!(error.is_fallback_safe());
605 assert!(!error.fallback_decision().sends_client_error());
606 }
607
608 fn recv_fd_and_token(stream: UnixStream) -> (RawFd, HandoffToken) {
609 let mut token_payload = [0_u8; 16];
610 let mut iov = libc::iovec {
611 iov_base: token_payload.as_mut_ptr().cast(),
612 iov_len: token_payload.len(),
613 };
614 let mut control = vec![0_u8; cmsg_space::<libc::c_int>()];
615 let mut message = unsafe { std::mem::zeroed::<libc::msghdr>() };
616 message.msg_iov = &mut iov;
617 message.msg_iovlen = 1;
618 message.msg_control = control.as_mut_ptr().cast();
619 message.msg_controllen = control.len() as _;
620
621 let received = unsafe { libc::recvmsg(stream.as_raw_fd(), &mut message, 0) };
622 assert_eq!(received as usize, token_payload.len());
623
624 let header = unsafe { libc::CMSG_FIRSTHDR(&message) };
625 assert!(!header.is_null());
626 unsafe {
627 assert_eq!((*header).cmsg_level, libc::SOL_SOCKET);
628 assert_eq!((*header).cmsg_type, libc::SCM_RIGHTS);
629 let received_fd = *libc::CMSG_DATA(header).cast::<libc::c_int>();
630 (received_fd, HandoffToken::from_bytes(token_payload))
631 }
632 }
633}