Skip to main content

kunobi_daemon/local/
unix.rs

1//! Unix stream adapters with peer checks and setup deadlines.
2
3use std::cell::Cell;
4use std::io::{self, Read, Write};
5use std::net::Shutdown;
6use std::os::unix::net::UnixStream;
7use std::sync::{
8    Arc, Mutex,
9    atomic::{AtomicBool, Ordering},
10};
11use std::time::{Duration, Instant};
12
13use super::{ConnectError, Endpoint};
14use crate::local::Duplex;
15use crate::transport::WriteHalf;
16
17/// A Unix connection whose setup deadline is shared by its independent halves.
18pub struct UnixDuplex {
19    stream: UnixStream,
20    read_deadline: Cell<Option<Instant>>,
21}
22
23#[cfg(target_os = "linux")]
24const CHILD_SIGNAL: std::os::raw::c_int = 17;
25#[cfg(not(target_os = "linux"))]
26const CHILD_SIGNAL: std::os::raw::c_int = 20;
27
28fn child_disposition(handler: usize) -> io::Result<usize> {
29    unsafe extern "C" {
30        fn signal(number: std::os::raw::c_int, handler: usize) -> usize;
31    }
32    // SAFETY: every caller passes SIG_DFL (0) or SIG_IGN (1), the two
33    // dispositions POSIX defines as constants rather than addresses, so no
34    // Rust code is ever installed on a signal stack. `signal` reports failure
35    // as SIG_ERR, checked below against `usize::MAX`.
36    let previous = unsafe { signal(CHILD_SIGNAL, handler) };
37    if previous == usize::MAX {
38        Err(io::Error::last_os_error())
39    } else {
40        Ok(previous)
41    }
42}
43
44/// The relay never waits for peer exit. Let the kernel reap it while this
45/// relay is idle on stdin, without another thread or periodic polling.
46pub fn auto_reap_children() -> io::Result<()> {
47    child_disposition(1).map(|_| ())
48}
49
50/// Restore normal child waiting in the peer: it owns and verifies candidate
51/// child processes. This only calls async-signal-safe signal(2).
52pub fn restore_child_waiting() -> io::Result<()> {
53    child_disposition(0).map(|_| ())
54}
55
56/// Spawn with normal child waiting, then restore the relay's reaping policy.
57///
58/// Rust may wait for a failed exec's child inside `Command::spawn`. Ignoring
59/// SIGCHLD during that wait makes it panic instead of returning the exec error.
60/// The peer also inherits normal waiting without a fork-only pre-exec hook.
61///
62/// The spawn waits while this crate creates a listening socket, so the child
63/// cannot inherit one that is not yet close-on-exec; see
64/// [`crate::local::unix_socket::acquire`].
65pub fn spawn_with_child_waiting(
66    command: &mut std::process::Command,
67) -> io::Result<std::process::Child> {
68    spawn_then(command, |_| Ok(())).map(|(child, ())| child)
69}
70
71/// Spawn as [`spawn_with_child_waiting`] does, and run `watch` on the child
72/// while SIGCHLD is still at its default. Until the child is waited for, its
73/// PID cannot name another process even if it has exited, so an exit handle
74/// opened here follows the child. With SIGCHLD ignored the kernel reaps the
75/// child as it exits, and the PID is free once that happens.
76///
77/// A failed `watch` stops the child and returns the error.
78fn spawn_then<T>(
79    command: &mut std::process::Command,
80    watch: impl FnOnce(&std::process::Child) -> io::Result<T>,
81) -> io::Result<(std::process::Child, T)> {
82    // Signal dispositions are process-wide. Serialize relay launches until
83    // both the previous policy and any exits during this window are handled.
84    let _spawn = crate::spawn_lock::spawning();
85    let previous = child_disposition(0)?;
86    let child = command.spawn().and_then(|child| watched(child, watch));
87    child_disposition(previous)?;
88    if previous == 1 {
89        reap_exited_children();
90    }
91    child
92}
93
94fn watched<T>(
95    mut child: std::process::Child,
96    watch: impl FnOnce(&std::process::Child) -> io::Result<T>,
97) -> io::Result<(std::process::Child, T)> {
98    match watch(&child) {
99        Ok(watching) => Ok((child, watching)),
100        Err(error) => {
101            let _ = child.kill();
102            let _ = child.wait();
103            Err(error)
104        }
105    }
106}
107
108fn reap_exited_children() {
109    unsafe extern "C" {
110        fn waitpid(
111            pid: std::os::raw::c_int,
112            status: *mut std::os::raw::c_int,
113            options: std::os::raw::c_int,
114        ) -> std::os::raw::c_int;
115    }
116    // A peer can exit while SIGCHLD is temporarily default. Reap those
117    // zombies after restoring SIG_IGN; subsequent exits are kernel-reaped.
118    loop {
119        // SAFETY: -1 selects any child of this process. POSIX allows a null
120        // `stat_loc`, which discards the exit status rather than writing
121        // through the pointer. WNOHANG is 1 on both supported targets, so the
122        // call returns immediately instead of blocking here.
123        let pid = unsafe { waitpid(-1, std::ptr::null_mut(), 1) };
124        if pid > 0 {
125            continue;
126        }
127        if pid == -1 && io::Error::last_os_error().kind() == io::ErrorKind::Interrupted {
128            continue;
129        }
130        break;
131    }
132}
133
134use std::os::fd::{AsRawFd, RawFd};
135
136/// The PID `LOCAL_PEERPID` reports: the socket's most recent owner.
137#[cfg(target_os = "macos")]
138fn macos_peer_pid(fd: RawFd) -> io::Result<u32> {
139    use std::os::raw::{c_int, c_void};
140    unsafe extern "C" {
141        fn getsockopt(
142            socket: c_int,
143            level: c_int,
144            name: c_int,
145            value: *mut c_void,
146            len: *mut u32,
147        ) -> c_int;
148    }
149    const SOL_LOCAL: c_int = 0;
150    const LOCAL_PEERPID: c_int = 0x002;
151    let mut pid: c_int = 0;
152    let mut len = std::mem::size_of::<c_int>() as u32;
153    // SAFETY: `pid` and `len` point to correctly sized writable values and fd
154    // remains owned by `self` for the duration of the call.
155    let rc = unsafe {
156        getsockopt(
157            fd,
158            SOL_LOCAL,
159            LOCAL_PEERPID,
160            (&raw mut pid).cast(),
161            &raw mut len,
162        )
163    };
164    if rc == 0 && pid > 0 {
165        Ok(pid as u32)
166    } else {
167        Err(io::Error::last_os_error())
168    }
169}
170
171/// [`Credentials`](crate::peer::Credentials) for a connected Unix socket. See
172/// [`super::peer`] for what they describe on each platform.
173#[cfg(not(target_os = "macos"))]
174pub(crate) fn fd_credentials(fd: RawFd) -> io::Result<crate::peer::Credentials> {
175    let (pid, uid) = peer_credentials(fd)?;
176    Ok(crate::peer::Credentials::new(
177        pid.and_then(crate::ProcessId::new),
178        uid == own_uid(),
179    ))
180}
181
182/// [`Credentials`](crate::peer::Credentials) for a connected Unix socket. See
183/// [`super::peer`] for what they describe on each platform.
184#[cfg(target_os = "macos")]
185pub(crate) fn macos_credentials(fd: RawFd) -> io::Result<crate::peer::Credentials> {
186    let same_user = macos_peer_uid(fd)? == own_uid();
187    let pid = crate::ProcessId::new(macos_peer_pid(fd)?);
188    Ok(crate::peer::Credentials::new(pid, same_user))
189}
190
191#[cfg(target_os = "macos")]
192pub(crate) use macos_credentials as fd_credentials;
193
194/// The peer's PID, when visible in this PID namespace, and its user ID.
195#[cfg(not(target_os = "macos"))]
196fn peer_credentials(fd: RawFd) -> io::Result<(Option<u32>, u32)> {
197    use std::os::raw::{c_int, c_void};
198    #[repr(C)]
199    struct UCred {
200        pid: c_int,
201        uid: u32,
202        gid: u32,
203    }
204    unsafe extern "C" {
205        fn getsockopt(
206            socket: c_int,
207            level: c_int,
208            name: c_int,
209            value: *mut c_void,
210            len: *mut u32,
211        ) -> c_int;
212    }
213    const SOL_SOCKET: c_int = 1;
214    const SO_PEERCRED: c_int = 17;
215    let mut cred = UCred {
216        pid: 0,
217        uid: 0,
218        gid: 0,
219    };
220    let mut len = std::mem::size_of::<UCred>() as u32;
221    // SAFETY: `cred` and `len` are correctly sized writable values and fd is
222    // a live Unix-domain socket owned by the caller.
223    let rc = unsafe {
224        getsockopt(
225            fd,
226            SOL_SOCKET,
227            SO_PEERCRED,
228            (&raw mut cred).cast(),
229            &raw mut len,
230        )
231    };
232    if rc != 0 {
233        return Err(io::Error::last_os_error());
234    }
235    Ok((visible_pid(cred.pid), cred.uid))
236}
237
238/// The PID from `SO_PEERCRED`, or `None` for a peer outside this PID namespace:
239/// Linux then reports PID 0 with a valid user ID.
240#[cfg(not(target_os = "macos"))]
241fn visible_pid(raw: std::os::raw::c_int) -> Option<u32> {
242    u32::try_from(raw).ok().filter(|pid| *pid > 0)
243}
244
245/// Current effective OS user ID.
246pub(crate) fn own_uid() -> u32 {
247    unsafe extern "C" {
248        fn geteuid() -> u32;
249    }
250    // SAFETY: geteuid takes no arguments and dereferences nothing. POSIX
251    // specifies it as always successful, so there is no error case and no
252    // errno to read afterwards.
253    unsafe { geteuid() }
254}
255
256/// Open or create a lock file private to the effective user.
257///
258/// The final component must not be a symlink and the file must belong to this
259/// user. Either refusal is `PermissionDenied`. The mode becomes 0600.
260pub(crate) fn open_private(path: &std::path::Path) -> io::Result<std::fs::File> {
261    use std::os::unix::fs::{MetadataExt, OpenOptionsExt, PermissionsExt};
262    let file = std::fs::OpenOptions::new()
263        .read(true)
264        .write(true)
265        .create(true)
266        .truncate(false)
267        .mode(0o600)
268        .custom_flags(libc::O_NOFOLLOW)
269        .open(path)
270        .map_err(|error| {
271            // O_NOFOLLOW reports a symlink as ELOOP.
272            if error.raw_os_error() == Some(libc::ELOOP) {
273                io::ErrorKind::PermissionDenied.into()
274            } else {
275                error
276            }
277        })?;
278    if file.metadata()?.uid() != own_uid() {
279        return Err(io::ErrorKind::PermissionDenied.into());
280    }
281    file.set_permissions(std::fs::Permissions::from_mode(0o600))?;
282    Ok(file)
283}
284
285/// The user `getpeereid` reports for a connected socket, captured at connect.
286#[cfg(target_os = "macos")]
287fn macos_peer_uid(fd: RawFd) -> io::Result<u32> {
288    unsafe extern "C" {
289        fn getpeereid(fd: RawFd, uid: *mut u32, gid: *mut u32) -> i32;
290    }
291    let (mut uid, mut gid) = (0, 0);
292    // SAFETY: fd is a live socket; both output pointers are valid u32 values.
293    if unsafe { getpeereid(fd, &mut uid, &mut gid) } != 0 {
294        return Err(io::Error::last_os_error());
295    }
296    Ok(uid)
297}
298
299/// What `kill(pid, 0)` and, on Linux, `/proc` establish about `pid`.
300///
301/// The fallback where no exit event is available: see
302/// [`super::ProcessHandle`]. Outside Linux it reads a child that exited but
303/// has not been waited for as alive.
304pub(crate) fn signal_state(pid: u32) -> super::ProcessState {
305    use super::ProcessState;
306    unsafe extern "C" {
307        fn kill(pid: i32, signal: i32) -> i32;
308    }
309    if pid == 0 || pid > i32::MAX as u32 {
310        return ProcessState::Unknown;
311    }
312    // A container's PID 1 may leave an exited orphan unreaped. kill(pid, 0)
313    // still succeeds for that zombie, although it cannot own any more work.
314    #[cfg(target_os = "linux")]
315    if std::fs::read_to_string(format!("/proc/{pid}/stat")).is_ok_and(|stat| {
316        stat.rsplit_once(") ")
317            .is_some_and(|(_, fields)| fields.starts_with("Z ") || fields.starts_with("X "))
318    }) {
319        return ProcessState::Exited;
320    }
321    // SAFETY: signal 0 checks process existence without delivering a signal.
322    if unsafe { kill(pid as i32, 0) } == 0 {
323        return ProcessState::Alive;
324    }
325    liveness_from_errno(io::Error::last_os_error().raw_os_error())
326}
327
328/// `kill(pid, 0)` failed: ESRCH means no such process, and EPERM means one
329/// exists that this user may not signal. Anything else proves nothing.
330fn liveness_from_errno(errno: Option<i32>) -> super::ProcessState {
331    use super::ProcessState;
332    const EPERM: i32 = 1;
333    const ESRCH: i32 = 3;
334    match errno {
335        Some(ESRCH) => ProcessState::Exited,
336        Some(EPERM) => ProcessState::Alive,
337        _ => ProcessState::Unknown,
338    }
339}
340
341/// Send SIGINT to every process in group `pgid`, as Ctrl-C in a terminal
342/// does to its foreground group. Test support for the launch module.
343#[cfg(all(test, feature = "launch"))]
344pub(crate) fn interrupt_process_group(pgid: u32) -> io::Result<()> {
345    unsafe extern "C" {
346        fn kill(pid: i32, signal: i32) -> i32;
347    }
348    const SIGINT: i32 = 2;
349    let pgid = i32::try_from(pgid).map_err(|_| io::ErrorKind::InvalidInput)?;
350    // SAFETY: a negative PID addresses the process group; the caller passes
351    // the ID of a group it created for the test.
352    if unsafe { kill(-pgid, SIGINT) } == 0 {
353        Ok(())
354    } else {
355        Err(io::Error::last_os_error())
356    }
357}
358
359/// Spawn `command` as the leader of a new session.
360///
361/// `setsid` in the child, before exec, gives it its own session and process
362/// group and no controlling terminal. So Ctrl-C in the terminal that started
363/// the caller does not reach it (SIGINT goes to the terminal's foreground
364/// process group), and neither does the hangup when that terminal closes. A
365/// new process group alone would stop the first but keep the terminal as the
366/// child's controlling terminal.
367///
368/// Every descriptor above stderr is also marked close-on-exec in the child, so
369/// the new program starts with only its standard streams. Without this a
370/// descriptor the caller left inheritable, such as a make jobserver pipe or a
371/// build tool's output pipe, stays open for the daemon's whole life, and the
372/// process waiting for its other end never sees EOF. The descriptors are
373/// marked rather than closed because the standard library reports a failed
374/// exec back to the parent through a close-on-exec pipe of its own; closing it
375/// before exec would make a missing binary look like a successful spawn.
376///
377/// Child waiting is handled as in [`spawn_with_child_waiting`].
378pub fn spawn_in_new_session(
379    command: &mut std::process::Command,
380) -> io::Result<std::process::Child> {
381    in_new_session(command, None);
382    spawn_with_child_waiting(command)
383}
384
385/// [`spawn_in_new_session`], with the child's exit handle opened before
386/// anything can reap the child. `keep` preserves the readiness channel across
387/// exec; the caller owns it and closes its copy after the spawn.
388#[cfg(feature = "launch")]
389pub(crate) fn spawn_in_new_session_watched(
390    command: &mut std::process::Command,
391    keep: Option<RawFd>,
392) -> io::Result<(std::process::Child, super::ProcessHandle)> {
393    in_new_session(command, keep);
394    spawn_then(command, super::ProcessHandle::for_child)
395}
396
397/// Make `command` lead a new session, preserving only standard descriptors
398/// and the optional readiness channel.
399fn in_new_session(command: &mut std::process::Command, keep: Option<RawFd>) {
400    use std::os::unix::process::CommandExt;
401    // Computed here, in the parent: getrlimit is not on the async-signal-safe
402    // list, and the child may only make such calls between fork and exec.
403    let descriptor_limit = descriptor_limit();
404    let prepare = move || {
405        // SAFETY: setsid takes no arguments and only changes the calling
406        // process's own session and process group. It cannot fail with EPERM
407        // here: a freshly forked child is never a process group leader.
408        if unsafe { libc::setsid() } == -1 {
409            return Err(io::Error::last_os_error());
410        }
411        mark_inherited_descriptors_cloexec(descriptor_limit);
412        if let Some(fd) = keep {
413            // SAFETY: F_SETFD with no flags only clears close-on-exec on this
414            // one descriptor, which the fork copied from the parent's open
415            // channel end. A number that is not open fails with EBADF.
416            checked(unsafe { libc::fcntl(fd, libc::F_SETFD, 0) })?;
417        }
418        Ok(())
419    };
420    // SAFETY: the hook runs in the forked child between fork and exec. It
421    // calls only setsid, close_range (Linux) and fcntl, which are
422    // async-signal-safe, and builds an `io::Error` from errno, which does not
423    // allocate. It touches no memory shared with the parent.
424    unsafe {
425        command.pre_exec(prepare);
426    }
427}
428
429/// A libc call's result, or the error it set. Only -1 reports failure.
430///
431/// Safe in a pre-exec hook: it reads `errno` and allocates nothing.
432fn checked(result: libc::c_int) -> io::Result<libc::c_int> {
433    if result == -1 {
434        Err(io::Error::last_os_error())
435    } else {
436        Ok(result)
437    }
438}
439
440/// A readiness channel: the launcher's end, and the daemon's end as a
441/// close-on-exec descriptor above stderr.
442///
443/// The daemon's end is moved to 3 or above, because a child's standard
444/// streams are set up before the descriptor sweep and would replace a channel
445/// at 0, 1 or 2, which a caller with a closed standard stream can be handed.
446///
447/// # Spawning while creating the pipe
448///
449/// Linux creates the pipe close-on-exec in one step (`pipe2`). macOS takes
450/// two, and a child spawned by another thread between them inherits both
451/// ends. A child holding the write end keeps the channel open for its whole
452/// life, so the launcher sees neither the daemon's exit nor its end of the
453/// channel.
454///
455/// The pipe is created while no spawn by this crate is under way, as a
456/// listener is in [`crate::local::unix_socket::acquire`]. Other spawns can
457/// still capture it. A process that spawns another way while it may be
458/// launching a daemon should have each child close, or mark close-on-exec,
459/// every descriptor above stderr before it execs.
460#[cfg(feature = "launch")]
461pub(crate) fn ready_pipe() -> io::Result<(std::io::PipeReader, std::os::fd::OwnedFd)> {
462    use std::os::fd::{FromRawFd, OwnedFd};
463    let (reader, writer) = crate::spawn_lock::without_spawns(std::io::pipe)?;
464    let writer = OwnedFd::from(writer);
465    // SAFETY: F_DUPFD_CLOEXEC only duplicates this process's open `writer`
466    // onto the lowest free number from 3 up, close-on-exec in the same step.
467    let raised = checked(unsafe { libc::fcntl(writer.as_raw_fd(), libc::F_DUPFD_CLOEXEC, 3) })?;
468    // SAFETY: fcntl returned a new open descriptor that nothing else owns.
469    Ok((reader, unsafe { OwnedFd::from_raw_fd(raised) }))
470}
471
472/// Take over the daemon's end of a readiness channel, named by the decimal
473/// descriptor number `value`.
474///
475/// Refuses, without touching it, anything but the write end of a pipe above
476/// stderr. The descriptor becomes close-on-exec, so programs the daemon
477/// starts do not hold the channel open.
478///
479/// # Safety
480///
481/// A descriptor `value` names must be this process's channel end, which
482/// nothing else in the process owns or closes: the returned writer closes it
483/// when dropped. The checks catch a value that names no pipe, not one that
484/// names some other pipe's write end.
485pub(crate) unsafe fn take_ready_descriptor(value: &str) -> io::Result<std::io::PipeWriter> {
486    use std::os::fd::{FromRawFd, OwnedFd};
487    let refuse = |message: &'static str| io::Error::new(io::ErrorKind::InvalidInput, message);
488    let fd: RawFd = value
489        .parse()
490        .map_err(|_| refuse("the readiness channel is not a descriptor number"))?;
491    if fd <= 2 {
492        return Err(refuse("a standard stream is not a readiness channel"));
493    }
494    let mut status = std::mem::MaybeUninit::<libc::stat>::uninit();
495    // SAFETY: fstat writes a `stat` into this valid slot, or fails with EBADF
496    // for a number that is not an open descriptor and writes nothing.
497    checked(unsafe { libc::fstat(fd, status.as_mut_ptr()) })?;
498    // SAFETY: fstat succeeded, so it initialised `status`.
499    let status = unsafe { status.assume_init() };
500    if (status.st_mode & libc::S_IFMT) != libc::S_IFIFO {
501        return Err(refuse("the readiness channel is not a pipe"));
502    }
503    // SAFETY: F_GETFL only reads the open file's status flags.
504    let flags = checked(unsafe { libc::fcntl(fd, libc::F_GETFL) })?;
505    if (flags & libc::O_ACCMODE) != libc::O_WRONLY {
506        return Err(refuse(
507            "the readiness channel is not the write end of a pipe",
508        ));
509    }
510    // SAFETY: F_SETFD sets only this descriptor's close-on-exec flag, the
511    // only descriptor flag there is.
512    checked(unsafe { libc::fcntl(fd, libc::F_SETFD, libc::FD_CLOEXEC) })?;
513    // SAFETY: the descriptor is open (checked above), and the caller
514    // guarantees that nothing else in this process owns it.
515    Ok(std::io::PipeWriter::from(unsafe {
516        OwnedFd::from_raw_fd(fd)
517    }))
518}
519
520/// The highest descriptor number to consider, from the soft RLIMIT_NOFILE.
521///
522/// An unlimited or very large limit is capped: the fallback loop costs one
523/// system call per number, and descriptors that high are not something a
524/// caller hands to a daemon by accident.
525fn descriptor_limit() -> libc::c_int {
526    const CAP: libc::rlim_t = 1 << 16;
527    let mut limit = libc::rlimit {
528        rlim_cur: 0,
529        rlim_max: 0,
530    };
531    // SAFETY: `limit` is a valid output slot for getrlimit.
532    let known = unsafe { libc::getrlimit(libc::RLIMIT_NOFILE, &mut limit) } == 0;
533    let current = if known { limit.rlim_cur } else { 1024 };
534    libc::c_int::try_from(current.min(CAP)).unwrap_or(1 << 16)
535}
536
537/// A pipe with neither end close-on-exec, as a build tool's jobserver or
538/// output pipe is when it runs a compiler that starts the daemon. Test support.
539#[cfg(all(test, feature = "launch"))]
540pub(crate) fn inheritable_pipe() -> (std::fs::File, std::fs::File) {
541    use std::os::fd::FromRawFd;
542    let mut fds = [0; 2];
543    // SAFETY: `fds` is a valid two-element output array for pipe(2).
544    assert_eq!(unsafe { libc::pipe(fds.as_mut_ptr()) }, 0);
545    // SAFETY: pipe succeeded, so both descriptors are open and now ours.
546    unsafe {
547        (
548            std::fs::File::from_raw_fd(fds[0]),
549            std::fs::File::from_raw_fd(fds[1]),
550        )
551    }
552}
553
554/// Mark every descriptor from 3 up close-on-exec. Async-signal-safe.
555fn mark_inherited_descriptors_cloexec(limit: libc::c_int) {
556    #[cfg(target_os = "linux")]
557    {
558        const CLOSE_RANGE_CLOEXEC: libc::c_uint = 1 << 2;
559        // SAFETY: close_range with CLOSE_RANGE_CLOEXEC only sets a flag on
560        // this process's descriptors; it frees nothing. Linux 5.11 and later
561        // support it; older kernels return ENOSYS or EINVAL and the loop
562        // below does the same job.
563        let done = unsafe {
564            libc::syscall(
565                libc::SYS_close_range,
566                3 as libc::c_uint,
567                libc::c_uint::MAX,
568                CLOSE_RANGE_CLOEXEC,
569            )
570        } == 0;
571        if done {
572            return;
573        }
574    }
575    for fd in 3..limit {
576        // SAFETY: fcntl on a number that is not an open descriptor fails with
577        // EBADF and changes nothing. FD_CLOEXEC is the only descriptor flag,
578        // so setting it outright loses nothing.
579        unsafe {
580            libc::fcntl(fd, libc::F_SETFD, libc::FD_CLOEXEC);
581        }
582    }
583}
584
585/// One-time compatibility fallback for a protocol-v1 peer that cannot drain:
586/// send it SIGTERM.
587///
588/// The signal reaches whichever process has `pid` now. Take the PID from the
589/// credentials of a live connection to that peer, and prefer draining: a PID
590/// can be reused once its process has exited.
591pub fn terminate_legacy_peer(pid: crate::ProcessId) -> io::Result<()> {
592    use std::os::raw::c_int;
593    unsafe extern "C" {
594        fn kill(pid: c_int, signal: c_int) -> c_int;
595    }
596    const SIGTERM: c_int = 15;
597    // A PID above i32::MAX would turn negative and address a process group.
598    let pid = c_int::try_from(pid.get())
599        .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "PID out of range"))?;
600    // SAFETY: kill takes two integers and dereferences nothing; `pid` is
601    // positive, so it names one process rather than a group.
602    if unsafe { kill(pid, SIGTERM) } == 0 {
603        Ok(())
604    } else {
605        Err(io::Error::last_os_error())
606    }
607}
608
609/// Wait for this process to be asked to stop.
610///
611/// The future completes on SIGTERM or SIGINT on Unix. On Windows it completes
612/// on Ctrl+C, or when the console closes, the user logs off or the system
613/// shuts down.
614///
615/// Handlers are installed before this returns, so a request that arrives
616/// afterwards is not lost even if the future has not been polled yet. On Unix,
617/// SIGTERM and SIGINT then no longer end the process by themselves, even after
618/// the future is dropped: the caller decides when to exit.
619///
620/// On Windows the future listens to Tokio's console streams. For close,
621/// logoff and shutdown Tokio's handler does not return while a listener
622/// exists, so the process runs until it exits on its own or until Windows'
623/// timeout for the event ends it, and `local::windows::session_end_requested`
624/// reports the event once the future completes on it.
625///
626/// Call it inside a Tokio runtime with I/O enabled (`enable_io` or
627/// `enable_all`); it panics outside one.
628#[cfg(feature = "async")]
629pub fn shutdown_signal() -> io::Result<impl Future<Output = ()> + Send + 'static> {
630    use tokio::signal::unix::{SignalKind, signal};
631    let mut terminate = signal(SignalKind::terminate())?;
632    let mut interrupt = signal(SignalKind::interrupt())?;
633    Ok(async move {
634        tokio::select! {
635            _ = terminate.recv() => {}
636            _ = interrupt.recv() => {}
637        }
638    })
639}
640
641impl super::peer::PeerCredentials for UnixDuplex {
642    fn credentials(&self) -> io::Result<crate::peer::Credentials> {
643        fd_credentials(self.stream.as_raw_fd())
644    }
645}
646
647impl Duplex for UnixDuplex {
648    type Reader = UnixReader;
649    type Writer = UnixWriter;
650
651    /// Make one connection attempt bounded by the caller's absolute deadline.
652    fn connect_once_until(path: &Endpoint, deadline: Instant) -> Result<Self, ConnectError> {
653        // `SockAddr::unix` would refuse it with an invalid argument, which read
654        // as a timeout and was retried until the deadline.
655        if crate::socket_path::check_socket_path(path).is_err() {
656            return Err(ConnectError::EndpointTooLong);
657        }
658        let connect = || -> io::Result<UnixStream> {
659            let remaining = deadline.saturating_duration_since(Instant::now());
660            if remaining.is_zero() {
661                return Err(io::ErrorKind::TimedOut.into());
662            }
663            let socket = socket2::Socket::new(socket2::Domain::UNIX, socket2::Type::STREAM, None)?;
664            socket.connect_timeout(&socket2::SockAddr::unix(path)?, remaining)?;
665            let fd: std::os::fd::OwnedFd = socket.into();
666            let stream = UnixStream::from(fd);
667            stream.peer_addr()?;
668            Ok(stream)
669        };
670        connect()
671            .map(|stream| Self {
672                stream,
673                read_deadline: Cell::new(None),
674            })
675            .map_err(|error| super::connect_error(&error))
676    }
677
678    /// Arm one absolute deadline for the whole session-establishment phase.
679    fn set_read_deadline(&self, timeout: Option<Duration>) -> io::Result<()> {
680        let deadline = timeout
681            .map(|duration| {
682                Instant::now().checked_add(duration).ok_or_else(|| {
683                    io::Error::new(io::ErrorKind::InvalidInput, "read deadline overflow")
684                })
685            })
686            .transpose()?;
687        self.stream.set_nonblocking(deadline.is_some())?;
688        self.read_deadline.set(deadline);
689        Ok(())
690    }
691
692    fn split(self) -> io::Result<(Self::Reader, Self::Writer)> {
693        let deadline = self.read_deadline.get();
694        let writer = self.stream.try_clone()?;
695        let setup = Arc::new(Setup {
696            active: AtomicBool::new(deadline.is_some()),
697            transition: Mutex::new(()),
698        });
699        Ok((
700            UnixReader {
701                stream: self.stream,
702                read_deadline: deadline,
703                setup: Arc::clone(&setup),
704            },
705            UnixWriter {
706                stream: writer,
707                deadline,
708                setup,
709            },
710        ))
711    }
712}
713
714// Only setup-mode transitions serialize with setup writes. Ordinary transport
715// I/O never acquires this mutex, and a read never holds a writer's lock.
716struct Setup {
717    active: AtomicBool,
718    transition: Mutex<()>,
719}
720
721/// Unix receive half with an optional absolute setup deadline.
722pub struct UnixReader {
723    stream: UnixStream,
724    read_deadline: Option<Instant>,
725    setup: Arc<Setup>,
726}
727
728impl UnixReader {
729    /// Return to ordinary blocking reads before entering the byte pump.
730    pub fn clear_read_deadline(&mut self) -> io::Result<()> {
731        let _transition = self
732            .setup
733            .transition
734            .lock()
735            .unwrap_or_else(|error| error.into_inner());
736        self.stream.set_nonblocking(false)?;
737        self.read_deadline = None;
738        self.setup.active.store(false, Ordering::Release);
739        Ok(())
740    }
741}
742
743impl Read for UnixReader {
744    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
745        let Some(deadline) = self.read_deadline else {
746            return self.stream.read(buf);
747        };
748        read_until(&self.stream, buf, deadline)
749    }
750}
751
752fn establishment_timeout() -> io::Error {
753    io::Error::new(
754        io::ErrorKind::TimedOut,
755        "Unix-socket session establishment timed out",
756    )
757}
758
759/// Unix send half; shutdown leaves the receive direction alive.
760pub struct UnixWriter {
761    stream: UnixStream,
762    deadline: Option<Instant>,
763    setup: Arc<Setup>,
764}
765
766impl Write for UnixWriter {
767    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
768        if !self.setup.active.load(Ordering::Acquire) {
769            return self.stream.write(buf);
770        }
771        let transition = self
772            .setup
773            .transition
774            .lock()
775            .unwrap_or_else(|error| error.into_inner());
776        if self.setup.active.load(Ordering::Acquire) {
777            let deadline = self.deadline.expect("setup has a deadline");
778            write_until(&self.stream, buf, deadline)
779        } else {
780            drop(transition);
781            self.stream.write(buf)
782        }
783    }
784    fn flush(&mut self) -> io::Result<()> {
785        self.stream.flush()
786    }
787}
788
789impl WriteHalf for UnixWriter {
790    fn shutdown_write(&mut self) -> io::Result<()> {
791        self.stream.shutdown(Shutdown::Write)
792    }
793}
794
795impl Read for UnixDuplex {
796    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
797        match self.read_deadline.get() {
798            Some(deadline) => read_until(&self.stream, buf, deadline),
799            None => self.stream.read(buf),
800        }
801    }
802}
803
804impl Write for UnixDuplex {
805    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
806        match self.read_deadline.get() {
807            Some(deadline) => write_until(&self.stream, buf, deadline),
808            None => self.stream.write(buf),
809        }
810    }
811    fn flush(&mut self) -> io::Result<()> {
812        self.stream.flush()
813    }
814}
815
816fn read_until(stream: &UnixStream, bytes: &mut [u8], deadline: Instant) -> io::Result<usize> {
817    if bytes.is_empty() {
818        return Ok(0);
819    }
820    let fd = stream.as_raw_fd();
821    bounded_io(fd, libc::POLLIN, deadline, || {
822        // SAFETY: the owned socket and writable slice stay alive through recv.
823        unsafe {
824            libc::recv(
825                fd,
826                bytes.as_mut_ptr().cast(),
827                bytes.len(),
828                libc::MSG_DONTWAIT,
829            )
830        }
831    })
832}
833fn write_until(stream: &UnixStream, bytes: &[u8], deadline: Instant) -> io::Result<usize> {
834    if bytes.is_empty() {
835        return Ok(0);
836    }
837    let fd = stream.as_raw_fd();
838    bounded_io(fd, libc::POLLOUT, deadline, || {
839        // SAFETY: the owned socket and immutable slice stay alive through send.
840        // Setup holds the socket in nonblocking mode; the transition guard
841        // prevents clearing that mode during this write. MSG_NOSIGNAL avoids SIGPIPE.
842        unsafe {
843            libc::send(
844                fd,
845                bytes.as_ptr().cast(),
846                bytes.len(),
847                libc::MSG_DONTWAIT | libc::MSG_NOSIGNAL,
848            )
849        }
850    })
851}
852fn bounded_io(
853    fd: RawFd,
854    events: libc::c_short,
855    deadline: Instant,
856    mut operation: impl FnMut() -> isize,
857) -> io::Result<usize> {
858    loop {
859        if Instant::now() >= deadline {
860            return Err(establishment_timeout());
861        }
862        let count = operation();
863        if count >= 0 {
864            if Instant::now() >= deadline {
865                return Err(establishment_timeout());
866            }
867            return Ok(count as usize);
868        }
869        let error = io::Error::last_os_error();
870        match error.kind() {
871            io::ErrorKind::Interrupted => continue,
872            io::ErrorKind::WouldBlock => {}
873            _ => return Err(error),
874        }
875        let remaining = deadline.saturating_duration_since(Instant::now());
876        let millis = remaining.as_millis().max(1).min(i32::MAX as u128) as i32;
877        let mut poll = libc::pollfd {
878            fd,
879            events,
880            revents: 0,
881        };
882        // SAFETY: poll receives one live, initialized entry for this owned socket.
883        let result = unsafe { libc::poll(&mut poll, 1, millis) };
884        if result < 0 {
885            let error = io::Error::last_os_error();
886            if error.kind() != io::ErrorKind::Interrupted {
887                return Err(error);
888            }
889        }
890        // recv/send remains nonblocking after readiness, including spurious wakeups.
891    }
892}
893
894#[cfg(test)]
895mod tests {
896    #[test]
897    fn only_minus_one_is_a_failed_call() {
898        assert_eq!(super::checked(0).unwrap(), 0);
899        assert_eq!(super::checked(1).unwrap(), 1);
900        assert_eq!(super::checked(3).unwrap(), 3);
901        assert!(super::checked(-1).is_err());
902    }
903
904    use super::*;
905    use crate::local::peer::PeerCredentials as _;
906    use std::os::unix::net::UnixListener;
907
908    #[test]
909    fn a_closed_peer_does_not_hide_the_buffered_final_response() {
910        let (stream, mut peer) = UnixStream::pair().unwrap();
911        let duplex = UnixDuplex {
912            stream,
913            read_deadline: Cell::new(None),
914        };
915        duplex
916            .set_read_deadline(Some(Duration::from_secs(1)))
917            .unwrap();
918        let (mut read, _write) = duplex.split().unwrap();
919        peer.write_all(b"final response").unwrap();
920        drop(peer);
921        let mut bytes = Vec::new();
922        read.read_to_end(&mut bytes).unwrap();
923        assert_eq!(bytes, b"final response");
924        read.clear_read_deadline().unwrap();
925    }
926
927    #[test]
928    fn an_unsplit_socket_also_honors_its_setup_deadline() {
929        let (stream, _peer) = UnixStream::pair().unwrap();
930        let mut duplex = UnixDuplex {
931            stream,
932            read_deadline: Cell::new(None),
933        };
934        duplex
935            .set_read_deadline(Some(Duration::from_millis(10)))
936            .unwrap();
937        let started = Instant::now();
938        assert_eq!(
939            duplex.read(&mut [0]).unwrap_err().kind(),
940            io::ErrorKind::TimedOut
941        );
942        assert!(started.elapsed() < Duration::from_secs(1));
943    }
944
945    #[test]
946    #[ignore = "child fixture for legacy peer termination"]
947    fn legacy_peer_waits_for_input() {
948        let mut byte = [0];
949        std::io::stdin().read_exact(&mut byte).unwrap();
950    }
951
952    #[test]
953    fn legacy_termination_signals_only_its_child_and_reports_os_failures() {
954        let child = std::process::Command::new(std::env::current_exe().unwrap())
955            .args([
956                "--exact",
957                "local::unix::tests::legacy_peer_waits_for_input",
958                "--ignored",
959            ])
960            .stdin(std::process::Stdio::piped())
961            .stdout(std::process::Stdio::null())
962            .stderr(std::process::Stdio::null())
963            .spawn()
964            .unwrap();
965        let mut child = crate::Candidate::new(child);
966        terminate_legacy_peer(child.id()).unwrap();
967        let status = child
968            .wait_until(Instant::now() + Duration::from_secs(10))
969            .unwrap()
970            .unwrap();
971        use std::os::unix::process::ExitStatusExt;
972        assert_eq!(status.signal(), Some(15));
973        let missing = crate::ProcessId::new(i32::MAX as u32).unwrap();
974        assert_eq!(
975            terminate_legacy_peer(missing).unwrap_err().raw_os_error(),
976            Some(3)
977        );
978    }
979
980    #[test]
981    fn terminating_a_pid_outside_the_process_range_is_refused() {
982        let error = terminate_legacy_peer(crate::ProcessId::new(u32::MAX).unwrap()).unwrap_err();
983        assert_eq!(error.kind(), io::ErrorKind::InvalidInput);
984    }
985
986    #[test]
987    fn process_state_reports_alive_exited_and_unknown() {
988        use super::super::{ProcessState, process_has_exited, process_state};
989        use crate::ProcessId;
990        assert_eq!(process_state(ProcessId::current()), ProcessState::Alive);
991        assert_eq!(
992            process_state(ProcessId::new(u32::MAX).unwrap()),
993            ProcessState::Unknown
994        );
995        let mut child = std::process::Command::new("true").spawn().unwrap();
996        let pid = ProcessId::of(&child);
997        child.wait().unwrap();
998        // Reaped, so the PID is gone (barring reuse within this test).
999        assert_eq!(process_state(pid), ProcessState::Exited);
1000        assert!(process_has_exited(pid));
1001    }
1002
1003    /// Hand `fd` back to an owner after a refused take.
1004    fn reclaim(fd: RawFd) {
1005        use std::os::fd::{FromRawFd, OwnedFd};
1006        // SAFETY: the test owned `fd` and the refused take did not close it.
1007        drop(unsafe { OwnedFd::from_raw_fd(fd) });
1008    }
1009
1010    #[test]
1011    fn only_the_write_end_of_a_pipe_is_taken_as_a_readiness_channel() {
1012        use std::os::fd::{IntoRawFd, OwnedFd};
1013        for value in ["", "x", "-1", "0", "1", "2"] {
1014            // SAFETY: refused before any descriptor is touched.
1015            let error = unsafe { take_ready_descriptor(value) }.unwrap_err();
1016            assert_eq!(error.kind(), io::ErrorKind::InvalidInput, "{value:?}");
1017        }
1018
1019        let file = tempfile::tempfile().unwrap().into_raw_fd();
1020        // SAFETY: the test owns `file`, and a refused take leaves it alone.
1021        let error = unsafe { take_ready_descriptor(&file.to_string()) }.unwrap_err();
1022        assert_eq!(error.kind(), io::ErrorKind::InvalidInput);
1023        reclaim(file);
1024
1025        let (reader, writer) = std::io::pipe().unwrap();
1026        let read_end = OwnedFd::from(reader).into_raw_fd();
1027        // SAFETY: the test owns `read_end`, and a refused take leaves it alone.
1028        let error = unsafe { take_ready_descriptor(&read_end.to_string()) }.unwrap_err();
1029        assert_eq!(error.kind(), io::ErrorKind::InvalidInput);
1030        reclaim(read_end);
1031        drop(writer);
1032    }
1033
1034    #[test]
1035    fn a_taken_readiness_channel_is_not_inherited_and_carries_messages() {
1036        use std::os::fd::{IntoRawFd, OwnedFd};
1037        let (mut reader, writer) = std::io::pipe().unwrap();
1038        let write_end = OwnedFd::from(writer);
1039        // SAFETY: clears close-on-exec on an open descriptor this test owns,
1040        // as the launcher's child sees its channel end after the exec.
1041        let cleared = unsafe { libc::fcntl(write_end.as_raw_fd(), libc::F_SETFD, 0) };
1042        assert_ne!(cleared, -1);
1043        let write_end = write_end.into_raw_fd();
1044        // SAFETY: the test gave up ownership of `write_end` just above.
1045        let mut pipe = unsafe { take_ready_descriptor(&write_end.to_string()) }.unwrap();
1046        // SAFETY: F_GETFD only reads the descriptor's flags.
1047        let flags = unsafe { libc::fcntl(write_end, libc::F_GETFD) };
1048        assert_ne!(
1049            flags & libc::FD_CLOEXEC,
1050            0,
1051            "programs the daemon starts would hold it"
1052        );
1053        pipe.write_all(b"ready\n").unwrap();
1054        let mut received = [0u8; 6];
1055        reader.read_exact(&mut received).unwrap();
1056        assert_eq!(&received, b"ready\n");
1057    }
1058
1059    #[cfg(feature = "launch")]
1060    #[test]
1061    fn a_readiness_pipe_leaves_the_standard_streams_alone() {
1062        let (_reader, child_end) = ready_pipe().unwrap();
1063        let fd = child_end.as_raw_fd();
1064        assert!(fd > 2, "{fd}");
1065        // SAFETY: F_GETFD only reads the descriptor's flags.
1066        let flags = unsafe { libc::fcntl(fd, libc::F_GETFD) };
1067        assert_ne!(
1068            flags & libc::FD_CLOEXEC,
1069            0,
1070            "another child could inherit it"
1071        );
1072    }
1073
1074    #[test]
1075    fn the_signal_fallback_reports_alive_exited_and_unknown() {
1076        use super::super::ProcessState;
1077        assert_eq!(signal_state(std::process::id()), ProcessState::Alive);
1078        assert_eq!(signal_state(0), ProcessState::Unknown);
1079        assert_eq!(signal_state(u32::MAX), ProcessState::Unknown);
1080        let mut child = std::process::Command::new("true").spawn().unwrap();
1081        let pid = child.id();
1082        child.wait().unwrap();
1083        assert_eq!(signal_state(pid), ProcessState::Exited);
1084    }
1085
1086    /// Linux reads a zombie's state from /proc, so the fallback reports it.
1087    #[cfg(target_os = "linux")]
1088    #[test]
1089    fn the_signal_fallback_reports_an_unreaped_child_as_exited_on_linux() {
1090        use super::super::{ProcessHandle, ProcessState};
1091        let mut child = std::process::Command::new("true").spawn().unwrap();
1092        ProcessHandle::for_child(&child).unwrap().wait().unwrap();
1093        assert_eq!(signal_state(child.id()), ProcessState::Exited);
1094        child.wait().unwrap();
1095    }
1096
1097    #[cfg(feature = "launch")]
1098    #[test]
1099    fn a_new_session_child_is_watched_from_its_spawn() {
1100        let mut command = std::process::Command::new("true");
1101        let (mut child, mut exit) = spawn_in_new_session_watched(&mut command, None).unwrap();
1102        assert_eq!(exit.pid(), crate::ProcessId::of(&child));
1103        exit.wait().unwrap();
1104        assert!(child.wait().unwrap().success());
1105    }
1106
1107    #[test]
1108    fn a_failed_watch_stops_the_child_and_returns_the_error() {
1109        let mut command = std::process::Command::new("sleep");
1110        command.arg("30");
1111        let mut pid = None;
1112        let error = spawn_then(&mut command, |child| {
1113            pid = Some(crate::ProcessId::of(child));
1114            Err::<(), _>(io::Error::other("watch failed"))
1115        })
1116        .unwrap_err();
1117        assert_eq!(error.to_string(), "watch failed");
1118        // Stopped and reaped: no process has the PID any more.
1119        assert_eq!(
1120            super::super::process_state(pid.unwrap()),
1121            super::super::ProcessState::Exited
1122        );
1123    }
1124
1125    #[test]
1126    fn a_new_session_child_leads_its_own_session_and_process_group() {
1127        unsafe extern "C" {
1128            fn getsid(pid: i32) -> i32;
1129            fn getpgid(pid: i32) -> i32;
1130        }
1131        let mut command = std::process::Command::new("sleep");
1132        command.arg("30");
1133        let mut child = spawn_in_new_session(&mut command).unwrap();
1134        let pid = child.id() as i32;
1135        // SAFETY: both only read the session and group IDs of a live PID
1136        // (our child, not yet waited) and of this process (0).
1137        let (child_sid, child_pgid, own_sid) = unsafe { (getsid(pid), getpgid(pid), getsid(0)) };
1138        child.kill().unwrap();
1139        child.wait().unwrap();
1140        assert_eq!(child_sid, pid, "not a session leader");
1141        assert_eq!(child_pgid, pid, "not a process group leader");
1142        assert_ne!(child_sid, own_sid, "still in the caller's session");
1143    }
1144
1145    #[test]
1146    fn a_process_this_user_may_not_signal_is_alive_not_exited() {
1147        use super::super::ProcessState;
1148        assert_eq!(liveness_from_errno(Some(1)), ProcessState::Alive);
1149        assert_eq!(liveness_from_errno(Some(3)), ProcessState::Exited);
1150        assert_eq!(liveness_from_errno(Some(22)), ProcessState::Unknown);
1151        assert_eq!(liveness_from_errno(None), ProcessState::Unknown);
1152    }
1153
1154    fn temp_socket(name: &str) -> std::path::PathBuf {
1155        let mut p = std::env::temp_dir();
1156        p.push(format!(
1157            "daemon-transport-test-{}-{}.sock",
1158            std::process::id(),
1159            name
1160        ));
1161        let _ = std::fs::remove_file(&p);
1162        p
1163    }
1164
1165    #[test]
1166    fn setup_write_deadline_bounds_a_peer_that_never_reads() {
1167        let path = temp_socket("write-deadline");
1168        let listener = std::os::unix::net::UnixListener::bind(&path).unwrap();
1169        let (release, wait) = std::sync::mpsc::channel();
1170        let server = std::thread::spawn(move || {
1171            let (_peer, _) = listener.accept().unwrap();
1172            let _ = wait.recv_timeout(Duration::from_secs(2));
1173        });
1174        let transport = UnixDuplex::connect(&path).unwrap();
1175        assert!(transport.credentials().unwrap().same_user);
1176        transport
1177            .set_read_deadline(Some(Duration::from_millis(100)))
1178            .unwrap();
1179        let (_read, mut write) = transport.split().unwrap();
1180        let error = write.write_all(&vec![0; 16 << 20]).unwrap_err();
1181        release.send(()).ok();
1182        server.join().unwrap();
1183        assert_eq!(error.kind(), io::ErrorKind::TimedOut);
1184        let _ = std::fs::remove_file(path);
1185    }
1186
1187    #[test]
1188    fn clearing_setup_restores_writes_after_the_original_deadline() {
1189        let (stream, mut peer) = UnixStream::pair().unwrap();
1190        let transport = UnixDuplex {
1191            stream,
1192            read_deadline: Cell::new(None),
1193        };
1194        assert!(transport.credentials().unwrap().same_user);
1195        assert!(fd_credentials(-1).is_err());
1196        transport
1197            .set_read_deadline(Some(Duration::from_millis(1)))
1198            .unwrap();
1199        let (mut read, mut write) = transport.split().unwrap();
1200        read.clear_read_deadline().unwrap();
1201        #[expect(
1202            clippy::disallowed_methods,
1203            reason = "The test must read after the cleared socket deadline has passed."
1204        )]
1205        std::thread::sleep(Duration::from_millis(5));
1206        write.write_all(b"long job").unwrap();
1207        let mut bytes = [0; 8];
1208        peer.read_exact(&mut bytes).unwrap();
1209        assert_eq!(&bytes, b"long job");
1210    }
1211
1212    #[test]
1213    fn an_absent_socket_times_out_rather_than_hanging() {
1214        let path = temp_socket("absent");
1215        let started = Instant::now();
1216        // A short budget: the point is that it gives up, not how long it waits.
1217        let err = UnixDuplex::connect_until(&path, Instant::now() + Duration::from_millis(150));
1218        assert_eq!(err.err(), Some(ConnectError::ConnectTimeout));
1219        assert!(
1220            started.elapsed() < Duration::from_secs(2),
1221            "connect did not respect its deadline"
1222        );
1223    }
1224
1225    #[test]
1226    fn an_overlong_endpoint_fails_at_once_instead_of_timing_out() {
1227        // It used to read as a timeout and be retried for the whole budget.
1228        let path = std::path::PathBuf::from(format!(
1229            "/{}",
1230            "a".repeat(super::super::unix_socket::MAX_PATH_BYTES)
1231        ));
1232        let started = Instant::now();
1233        let err = UnixDuplex::connect_until(&path, Instant::now() + Duration::from_secs(5));
1234        assert_eq!(err.err(), Some(ConnectError::EndpointTooLong));
1235        assert!(
1236            started.elapsed() < Duration::from_millis(500),
1237            "it retried a path that cannot work"
1238        );
1239    }
1240
1241    #[test]
1242    fn a_listening_socket_connects_and_splits() {
1243        let path = temp_socket("listen");
1244        let listener = UnixListener::bind(&path).unwrap();
1245
1246        let accept = std::thread::spawn(move || listener.accept().map(|(s, _)| s));
1247
1248        let duplex = UnixDuplex::connect(&path).expect("connect");
1249        let (mut r, mut w) = duplex.split().expect("split");
1250
1251        let mut server = accept.join().unwrap().unwrap();
1252
1253        w.write_all(b"ping").unwrap();
1254        w.shutdown_write().unwrap();
1255
1256        let mut got = [0u8; 4];
1257        server.read_exact(&mut got).unwrap();
1258        assert_eq!(&got, b"ping");
1259
1260        // The half-close must not have torn down the reply direction.
1261        server.write_all(b"pong").unwrap();
1262        drop(server);
1263
1264        let mut back = Vec::new();
1265        r.read_to_end(&mut back).unwrap();
1266        assert_eq!(back, b"pong");
1267
1268        let _ = std::fs::remove_file(&path);
1269    }
1270
1271    #[test]
1272    fn the_two_halves_are_independent_file_descriptors() {
1273        // If `split` handed back the same fd behind a lock, the parked read
1274        // below would block the write and this test would hang.
1275        let path = temp_socket("split");
1276        let listener = UnixListener::bind(&path);
1277        let accept = std::thread::spawn(move || listener.unwrap().accept().map(|(s, _)| s));
1278
1279        let (mut r, mut w) = UnixDuplex::connect(&path).unwrap().split().unwrap();
1280        let mut server = accept.join().unwrap().unwrap();
1281
1282        let reader = std::thread::spawn(move || {
1283            let mut buf = [0u8; 4];
1284            r.read_exact(&mut buf).map(|_| buf)
1285        });
1286
1287        // Written while the reader thread is parked in `read`.
1288        w.write_all(b"up__").unwrap();
1289        let mut got = [0u8; 4];
1290        server.read_exact(&mut got).unwrap();
1291        assert_eq!(&got, b"up__");
1292
1293        server.write_all(b"down").unwrap();
1294        assert_eq!(&reader.join().unwrap().unwrap(), b"down");
1295
1296        let _ = std::fs::remove_file(&path);
1297    }
1298
1299    #[test]
1300    fn a_deadline_bounds_a_peer_that_never_answers_the_preamble() {
1301        // The wedge this exists for: the kernel completed the connection, so
1302        // `connect` succeeded, but nothing ever writes an acknowledgement.
1303        // Without the deadline the relay would park here forever during client setup.
1304        let path = temp_socket("silent");
1305        let listener = UnixListener::bind(&path).unwrap();
1306        let accept = std::thread::spawn(move || listener.accept().map(|(s, _)| s));
1307
1308        let duplex = UnixDuplex::connect(&path).expect("the kernel-level connect succeeds");
1309        duplex
1310            .set_read_deadline(Some(Duration::from_millis(100)))
1311            .expect("arm");
1312        let mut server = accept.join().unwrap().unwrap();
1313
1314        let (mut r, mut w) = duplex.split().expect("split");
1315        let started = Instant::now();
1316        let outcome = crate::local::test_handshake(&mut w, &mut r, "test");
1317        let elapsed = started.elapsed();
1318
1319        assert!(outcome.is_err(), "a silent peer must not pass");
1320        assert!(
1321            elapsed < Duration::from_secs(2),
1322            "handshake ignored its deadline: {elapsed:?}"
1323        );
1324        // Disarming restores ordinary pump semantics: EOF arrives when the
1325        // peer closes, rather than every read failing with a timeout error.
1326        r.clear_read_deadline().unwrap();
1327        // The server never read our preamble. On Linux, dropping a socket
1328        // that still holds unread data surfaces to the peer as ECONNRESET
1329        // rather than EOF - so drain exactly what was written before closing,
1330        // and the client sees the clean EOF a healthy teardown produces.
1331        // `PREAMBLE_LEN` is the fixed wire size INCLUDING the identity.
1332        let mut drained = 0;
1333        while drained < 6 {
1334            match server.read(&mut [0u8; 64]) {
1335                Ok(0) => break,
1336                Ok(n) => drained += n,
1337                Err(e) => panic!("server drain failed: {e}"),
1338            }
1339        }
1340        drop(server);
1341        let mut sink = [0u8; 1];
1342        assert_eq!(r.read(&mut sink).unwrap(), 0);
1343
1344        let _ = std::fs::remove_file(&path);
1345    }
1346
1347    #[test]
1348    fn a_drip_fed_ack_cannot_extend_the_establishment_deadline() {
1349        let path = temp_socket("drip");
1350        let listener = UnixListener::bind(&path).unwrap();
1351        let server = std::thread::spawn(move || {
1352            let (mut connection, _) = listener.accept().unwrap();
1353            let mut preamble = [0u8; 6];
1354            connection.read_exact(&mut preamble).unwrap();
1355            for byte in *b"ready\n" {
1356                if connection.write_all(&[byte]).is_err() {
1357                    break;
1358                }
1359                connection.flush().unwrap();
1360                #[expect(
1361                    clippy::disallowed_methods,
1362                    reason = "The fixture trickles bytes to verify the handshake uses one total deadline."
1363                )]
1364                std::thread::sleep(Duration::from_millis(30));
1365            }
1366        });
1367
1368        let duplex = UnixDuplex::connect(&path).unwrap();
1369        duplex
1370            .set_read_deadline(Some(Duration::from_millis(150)))
1371            .unwrap();
1372        let (mut read, mut write) = duplex.split().unwrap();
1373        let started = Instant::now();
1374        let outcome = crate::local::test_handshake(&mut write, &mut read, "test");
1375        let elapsed = started.elapsed();
1376
1377        assert!(outcome.is_err(), "a slow drip must not reset the deadline");
1378        assert!(
1379            elapsed < Duration::from_millis(350),
1380            "drip feed extended the absolute deadline: {elapsed:?}"
1381        );
1382        server.join().unwrap();
1383        let _ = std::fs::remove_file(&path);
1384    }
1385
1386    #[test]
1387    fn a_socket_with_no_permissions_is_reported_as_permission_denied() {
1388        // Mode 0000 is enforced on connect (verified on macOS 26.5), and this
1389        // must short-circuit rather than spend the whole retry budget.
1390        use std::os::unix::fs::PermissionsExt;
1391
1392        let path = temp_socket("perms");
1393        let _listener = UnixListener::bind(&path).unwrap();
1394        std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o000)).unwrap();
1395
1396        let started = Instant::now();
1397        let result = UnixDuplex::connect(&path);
1398        let elapsed = started.elapsed();
1399
1400        // Root ignores file modes, so on a root CI container this legitimately
1401        // connects; the assertion is conditional rather than flaky.
1402        if let Err(e) = result {
1403            assert_eq!(e, ConnectError::PermissionDenied);
1404            assert!(
1405                elapsed < crate::local::CONNECT_BUDGET,
1406                "permission failure burned the retry budget instead of short-circuiting"
1407            );
1408        }
1409
1410        let _ = std::fs::remove_file(&path);
1411    }
1412
1413    #[test]
1414    fn a_live_endpoint_connect_once_does_not_retry_sleep() {
1415        use super::super::{Duplex, RETRY_INTERVAL};
1416
1417        let path = temp_socket("live-once");
1418        let listener = UnixListener::bind(&path).unwrap();
1419        let accept = std::thread::spawn(move || listener.accept());
1420        let started = std::time::Instant::now();
1421        let duplex = UnixDuplex::connect_once(&path).expect("live endpoint");
1422        let elapsed = started.elapsed();
1423        assert!(
1424            elapsed < RETRY_INTERVAL,
1425            "live connect inserted a retry sleep: {elapsed:?}"
1426        );
1427        drop(duplex);
1428        let _ = accept.join();
1429        let _ = std::fs::remove_file(&path);
1430    }
1431}
1432
1433#[cfg(all(test, not(target_os = "macos")))]
1434mod visible_pid_tests {
1435    use super::visible_pid;
1436
1437    #[test]
1438    fn pid_zero_and_negative_values_are_not_visible_pids() {
1439        assert_eq!(visible_pid(0), None);
1440        assert_eq!(visible_pid(-1), None);
1441        assert_eq!(visible_pid(42), Some(42));
1442    }
1443}