Skip to main content

monitord/varlink/
machine_connector.rs

1//! Varlink connections into machines, made from inside their PID namespace.
2//!
3//! systemd's credential-checking varlink servers (PID 1, networkd) refuse
4//! peers they cannot see in their own PID namespace: `SO_PEERCRED` reports
5//! pid 0 for a process in an ancestor namespace and the server closes the
6//! connection (see #211 and systemd/systemd#43807). Reaching a machine's
7//! sockets through `/proc/<leader>/root` is therefore not enough — the
8//! `connect()` itself has to happen inside the machine's PID namespace.
9//!
10//! Joining a PID namespace needs `CAP_SYS_ADMIN` (and opening it
11//! `CAP_SYS_PTRACE`), and `setns(CLONE_NEWPID)` only moves future children.
12//! So per machine we spawn one small helper — monitord re-executed with
13//! [`HELPER_ARG`] — from a short-lived thread that joined the namespace. The
14//! helper opens the machine's root directory, drops to an unprivileged user
15//! without any capabilities, and then serves connect requests over a
16//! socketpair: monitord names one of a fixed set of sockets
17//! ([`MachineSocket`]) and receives a connected fd back via `SCM_RIGHTS`. The
18//! kernel records peer credentials at `connect()` time, so the fd keeps
19//! working in monitord, which never leaves its own namespaces.
20//!
21//! Connectors are cached per machine and leader PID (see `crate::machines`),
22//! so a machine costs one helper spawn rather than one per collection cycle,
23//! and each varlink connection only a request round trip over the socketpair.
24
25use std::fmt;
26use std::io;
27use std::os::fd::OwnedFd;
28use std::sync::{Arc, Mutex};
29use std::time::Duration;
30
31use anyhow::Context;
32use thiserror::Error;
33
34/// Hidden first argument that makes monitord run as a machine connector
35/// helper instead of a collector. Dispatched from `main` before the tokio
36/// runtime starts; see [`helper_main`].
37pub const HELPER_ARG: &str = "__monitord-machine-connector";
38
39/// The machine sockets a connector may connect to.
40///
41/// A fixed allowlist sent as a single byte, rather than arbitrary paths, so a
42/// helper can only ever be used to reach these read-mostly systemd endpoints.
43#[derive(Clone, Copy, Debug, PartialEq, Eq)]
44#[repr(u8)]
45pub enum MachineSocket {
46    /// PID 1's `io.systemd.Manager`, `io.systemd.Unit` and `io.systemd.Job`.
47    Manager = 1,
48    /// PID 1's `io.systemd.Metrics` report socket.
49    Metrics = 2,
50    /// systemd-networkd's `io.systemd.Network`.
51    Network = 3,
52}
53
54impl MachineSocket {
55    pub const ALL: [MachineSocket; 3] = [Self::Manager, Self::Metrics, Self::Network];
56
57    /// Socket path inside the machine.
58    pub fn path(self) -> &'static str {
59        match self {
60            Self::Manager => crate::varlink::manager::MANAGER_SOCKET_PATH,
61            Self::Metrics => crate::varlink_units::METRICS_SOCKET_PATH,
62            Self::Network => crate::varlink::network::NETWORK_SOCKET_PATH,
63        }
64    }
65
66    fn from_byte(byte: u8) -> Option<Self> {
67        Self::ALL.into_iter().find(|socket| *socket as u8 == byte)
68    }
69}
70
71#[derive(Error, Debug)]
72pub enum MachineConnectorError {
73    /// monitord lacks the privileges to join the machine's PID namespace.
74    /// Won't change at runtime, so callers stop trying for every machine.
75    #[error(
76        "joining the PID namespace of machine leader {leader_pid} was refused ({source}); \
77         varlink collection from machines needs CAP_SYS_ADMIN and CAP_SYS_PTRACE"
78    )]
79    PermissionDenied { leader_pid: u32, source: io::Error },
80    #[error("machine connector for leader {leader_pid}: {source}")]
81    Io { leader_pid: u32, source: io::Error },
82    #[error("machine connectors are only supported on Linux")]
83    Unsupported,
84}
85
86/// A helper inside one machine's PID namespace handing out connected sockets.
87pub struct MachineConnector {
88    leader_pid: u32,
89    control: Mutex<OwnedFd>,
90    child: Mutex<std::process::Child>,
91}
92
93impl fmt::Debug for MachineConnector {
94    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
95        f.debug_struct("MachineConnector")
96            .field("leader_pid", &self.leader_pid)
97            .finish_non_exhaustive()
98    }
99}
100
101impl MachineConnector {
102    /// Spawn a connector for the machine whose leader is `leader_pid`.
103    ///
104    /// Blocking: joins the namespace on a short-lived thread, spawns the
105    /// helper and waits (at most `timeout`) until it reports that it opened
106    /// the machine's root and dropped its privileges.
107    pub fn spawn(leader_pid: u32, timeout: Duration) -> Result<Self, MachineConnectorError> {
108        sys::spawn(leader_pid, timeout)
109    }
110
111    pub fn leader_pid(&self) -> u32 {
112        self.leader_pid
113    }
114
115    /// Whether the helper is still running. It only exits on its own when
116    /// its machine goes away or on error, so a cached connector that is no
117    /// longer alive must be replaced rather than reused.
118    pub fn is_alive(&self) -> bool {
119        let mut child = self.child.lock().unwrap_or_else(|e| e.into_inner());
120        matches!(child.try_wait(), Ok(None))
121    }
122
123    /// Connect to `socket` inside the machine, blocking for the round trip.
124    pub fn connect_blocking(
125        &self,
126        socket: MachineSocket,
127    ) -> Result<std::os::unix::net::UnixStream, MachineConnectorError> {
128        let control = self.control.lock().unwrap_or_else(|e| e.into_inner());
129        sys::request(&control, socket).map_err(|source| MachineConnectorError::Io {
130            leader_pid: self.leader_pid,
131            source,
132        })
133    }
134
135    /// Connect to `socket` inside the machine and wrap it for zlink.
136    ///
137    /// The tokio stream is created on the calling runtime, so this also works
138    /// from the per-call current-thread runtimes the streaming collectors use.
139    pub async fn connect(
140        self: &Arc<Self>,
141        socket: MachineSocket,
142    ) -> anyhow::Result<zlink::tokio::unix::Connection> {
143        let this = Arc::clone(self);
144        let stream = tokio::task::spawn_blocking(move || this.connect_blocking(socket)).await??;
145        let stream = stream.try_into().with_context(|| {
146            format!(
147                "machine connector for leader {}: wrapping {socket:?} socket for zlink",
148                self.leader_pid
149            )
150        })?;
151        Ok(zlink::tokio::unix::Connection::new(stream))
152    }
153}
154
155impl Drop for MachineConnector {
156    fn drop(&mut self) {
157        // The helper also exits on EOF once `control` closes; killing it
158        // just makes that immediate, and waiting reaps it.
159        let child = self.child.get_mut().unwrap_or_else(|e| e.into_inner());
160        let _ = child.kill();
161        let _ = child.wait();
162    }
163}
164
165/// Entry point of the helper process. Never returns.
166pub fn helper_main(args: &[String]) -> ! {
167    let code = match sys::helper_run(args) {
168        Ok(()) => 0,
169        Err(err) => {
170            eprintln!("monitord machine connector: {err}");
171            1
172        }
173    };
174    std::process::exit(code)
175}
176
177/// Whether `err` means monitord cannot join machine namespaces at all.
178pub fn is_permission_denied(err: &anyhow::Error) -> bool {
179    matches!(
180        err.downcast_ref::<MachineConnectorError>(),
181        Some(MachineConnectorError::PermissionDenied { .. })
182    )
183}
184
185#[cfg(target_os = "linux")]
186mod sys {
187    use std::fs::File;
188    use std::io;
189    use std::os::fd::{AsFd, AsRawFd, BorrowedFd, FromRawFd, OwnedFd, RawFd};
190    use std::os::unix::fs::OpenOptionsExt;
191    use std::os::unix::net::UnixStream;
192    use std::process::{Command, Stdio};
193    use std::sync::Mutex;
194    use std::time::Duration;
195
196    use super::{MachineConnector, MachineConnectorError, MachineSocket, HELPER_ARG};
197
198    /// The helper never connects as root: if monitord runs as root, the
199    /// helper drops to nobody, so a machine always sees an unprivileged peer.
200    const NOBODY: libc::uid_t = 65534;
201
202    pub(super) fn spawn(
203        leader_pid: u32,
204        timeout: Duration,
205    ) -> Result<MachineConnector, MachineConnectorError> {
206        let io_err = |source| MachineConnectorError::Io { leader_pid, source };
207        let perm_or_io = |source: io::Error| match source.raw_os_error() {
208            Some(libc::EPERM) | Some(libc::EACCES) => {
209                MachineConnectorError::PermissionDenied { leader_pid, source }
210            }
211            _ => MachineConnectorError::Io { leader_pid, source },
212        };
213
214        // Opening another user's namespace needs ptrace access (CAP_SYS_PTRACE).
215        let pidns = File::open(format!("/proc/{leader_pid}/ns/pid")).map_err(perm_or_io)?;
216        let (control, helper_end) = seqpacket_pair().map_err(io_err)?;
217        set_recv_timeout(control.as_fd(), timeout).map_err(io_err)?;
218
219        // setns(CLONE_NEWPID) only changes the PID namespace of the calling
220        // thread's future children, so join it on a throwaway thread and spawn
221        // the helper from there: the helper starts inside the machine's PID
222        // namespace, and the change dies with the thread instead of lingering
223        // on a runtime worker.
224        let child = std::thread::scope(|scope| {
225            scope
226                .spawn(|| -> io::Result<std::process::Child> {
227                    // SAFETY: plain syscall on a valid fd we own.
228                    if unsafe { libc::setns(pidns.as_raw_fd(), libc::CLONE_NEWPID) } != 0 {
229                        return Err(io::Error::last_os_error());
230                    }
231                    Command::new("/proc/self/exe")
232                        .arg(HELPER_ARG)
233                        .arg(leader_pid.to_string())
234                        .env_clear()
235                        .stdin(Stdio::from(helper_end))
236                        .stdout(Stdio::null())
237                        .stderr(Stdio::inherit())
238                        .spawn()
239                })
240                .join()
241                .unwrap_or_else(|_| Err(io::Error::other("namespace join thread panicked")))
242        })
243        .map_err(perm_or_io)?;
244
245        let connector = MachineConnector {
246            leader_pid,
247            control: Mutex::new(control),
248            child: Mutex::new(child),
249        };
250
251        // Wait for the helper's ready message: root opened, privileges dropped.
252        let control = connector.control.lock().unwrap_or_else(|e| e.into_inner());
253        let (status, _) = recv_status(control.as_fd()).map_err(io_err)?;
254        drop(control);
255        if status != 0 {
256            return Err(perm_or_io(io::Error::from_raw_os_error(status.into())));
257        }
258        Ok(connector)
259    }
260
261    pub(super) fn request(control: &OwnedFd, socket: MachineSocket) -> io::Result<UnixStream> {
262        let byte = [socket as u8];
263        // SAFETY: valid fd and a 1 byte buffer that outlives the call.
264        let sent = unsafe {
265            libc::send(
266                control.as_raw_fd(),
267                byte.as_ptr().cast(),
268                byte.len(),
269                libc::MSG_NOSIGNAL,
270            )
271        };
272        if sent < 0 {
273            return Err(io::Error::last_os_error());
274        }
275        match recv_status(control.as_fd())? {
276            (0, Some(fd)) => Ok(UnixStream::from(fd)),
277            (0, None) => Err(io::Error::other("connector replied without a socket")),
278            (errno, _) => Err(io::Error::from_raw_os_error(errno.into())),
279        }
280    }
281
282    pub(super) fn helper_run(args: &[String]) -> io::Result<()> {
283        let leader_pid: u32 = args
284            .first()
285            .and_then(|arg| arg.parse().ok())
286            .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "missing leader PID"))?;
287        // SAFETY: the parent passes the helper's end of the socketpair as stdin.
288        let control = unsafe { OwnedFd::from_raw_fd(0) };
289
290        let root = match open_root(leader_pid).and_then(|root| drop_privileges().map(|()| root)) {
291            Ok(root) => root,
292            Err(err) => {
293                let _ = send_status(control.as_fd(), errno_byte(&err), None);
294                return Err(err);
295            }
296        };
297        send_status(control.as_fd(), 0, None)?;
298        serve(control.as_fd(), root.as_fd())
299    }
300
301    /// Answer connect requests until monitord closes its end.
302    ///
303    /// Separate from [`helper_run`] so it can be tested without privileges,
304    /// against any directory standing in for a machine's root.
305    pub(super) fn serve(control: BorrowedFd<'_>, root: BorrowedFd<'_>) -> io::Result<()> {
306        loop {
307            let mut byte = [0u8; 1];
308            // SAFETY: valid fd and a 1 byte buffer that outlives the call.
309            let n = unsafe { libc::recv(control.as_raw_fd(), byte.as_mut_ptr().cast(), 1, 0) };
310            match n {
311                0 => return Ok(()),
312                n if n < 0 => {
313                    let err = io::Error::last_os_error();
314                    if err.kind() == io::ErrorKind::Interrupted {
315                        continue;
316                    }
317                    return Err(err);
318                }
319                _ => {}
320            }
321            let Some(socket) = MachineSocket::from_byte(byte[0]) else {
322                send_status(control, libc::EINVAL as u8, None)?;
323                continue;
324            };
325            match connect_under(root, socket.path()) {
326                Ok(stream) => send_status(control, 0, Some(stream.as_fd()))?,
327                Err(err) => send_status(control, errno_byte(&err), None)?,
328            }
329        }
330    }
331
332    /// Connect to `path` below `root` without needing access to the machine
333    /// leader's procfs entry: `/proc/self/fd/<root>` is always our own.
334    fn connect_under(root: BorrowedFd<'_>, path: &str) -> io::Result<UnixStream> {
335        UnixStream::connect(format!("/proc/self/fd/{}{}", root.as_raw_fd(), path))
336    }
337
338    fn open_root(leader_pid: u32) -> io::Result<OwnedFd> {
339        let root = std::fs::OpenOptions::new()
340            .read(true)
341            .custom_flags(libc::O_PATH | libc::O_DIRECTORY)
342            .open(format!("/proc/{leader_pid}/root"))?;
343        Ok(root.into())
344    }
345
346    /// Become unprivileged for good: no root identity, no capabilities, and
347    /// no way to regain any.
348    fn drop_privileges() -> io::Result<()> {
349        // SAFETY: plain syscalls with valid arguments.
350        unsafe {
351            if libc::getuid() == 0 || libc::geteuid() == 0 {
352                check(libc::setgroups(0, std::ptr::null()))?;
353                check(libc::setresgid(NOBODY, NOBODY, NOBODY))?;
354                check(libc::setresuid(NOBODY, NOBODY, NOBODY))?;
355            }
356            check(libc::prctl(
357                libc::PR_CAP_AMBIENT,
358                libc::PR_CAP_AMBIENT_CLEAR_ALL,
359                0,
360                0,
361                0,
362            ))?;
363            #[repr(C)]
364            struct CapHeader {
365                version: u32,
366                pid: libc::c_int,
367            }
368            #[repr(C)]
369            struct CapData {
370                effective: u32,
371                permitted: u32,
372                inheritable: u32,
373            }
374            const LINUX_CAPABILITY_VERSION_3: u32 = 0x2008_0522;
375            let header = CapHeader {
376                version: LINUX_CAPABILITY_VERSION_3,
377                pid: 0,
378            };
379            let data = [
380                CapData {
381                    effective: 0,
382                    permitted: 0,
383                    inheritable: 0,
384                },
385                CapData {
386                    effective: 0,
387                    permitted: 0,
388                    inheritable: 0,
389                },
390            ];
391            if libc::syscall(libc::SYS_capset, &header, data.as_ptr()) != 0 {
392                return Err(io::Error::last_os_error());
393            }
394            check(libc::prctl(libc::PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0))?;
395            // The helper lives inside the machine's PID namespace, where
396            // processes running under the same numeric UID could otherwise
397            // ptrace it (dropping from root already clears this flag).
398            check(libc::prctl(libc::PR_SET_DUMPABLE, 0, 0, 0, 0))?;
399        }
400        Ok(())
401    }
402
403    fn check(ret: libc::c_int) -> io::Result<()> {
404        if ret < 0 {
405            Err(io::Error::last_os_error())
406        } else {
407            Ok(())
408        }
409    }
410
411    fn errno_byte(err: &io::Error) -> u8 {
412        err.raw_os_error()
413            .and_then(|errno| u8::try_from(errno).ok())
414            .filter(|errno| *errno != 0)
415            .unwrap_or(libc::EIO as u8)
416    }
417
418    pub(super) fn seqpacket_pair() -> io::Result<(OwnedFd, OwnedFd)> {
419        let mut fds = [0 as RawFd; 2];
420        // SAFETY: fds has room for the two descriptors socketpair returns.
421        check(unsafe {
422            libc::socketpair(
423                libc::AF_UNIX,
424                libc::SOCK_SEQPACKET | libc::SOCK_CLOEXEC,
425                0,
426                fds.as_mut_ptr(),
427            )
428        })?;
429        // SAFETY: socketpair succeeded, so both are fresh fds we now own.
430        Ok(unsafe { (OwnedFd::from_raw_fd(fds[0]), OwnedFd::from_raw_fd(fds[1])) })
431    }
432
433    fn set_recv_timeout(fd: BorrowedFd<'_>, timeout: Duration) -> io::Result<()> {
434        let tv = libc::timeval {
435            tv_sec: timeout.as_secs().try_into().unwrap_or(libc::time_t::MAX),
436            tv_usec: timeout.subsec_micros().into(),
437        };
438        // SAFETY: valid fd and a timeval that outlives the call.
439        check(unsafe {
440            libc::setsockopt(
441                fd.as_raw_fd(),
442                libc::SOL_SOCKET,
443                libc::SO_RCVTIMEO,
444                std::ptr::from_ref(&tv).cast(),
445                std::mem::size_of::<libc::timeval>() as libc::socklen_t,
446            )
447        })
448    }
449
450    /// Room for one fd worth of `SCM_RIGHTS`, suitably aligned.
451    #[repr(C, align(8))]
452    struct CmsgBuf([u8; 64]);
453
454    /// Send a status byte, optionally with one fd attached.
455    pub(super) fn send_status(
456        sock: BorrowedFd<'_>,
457        status: u8,
458        fd: Option<BorrowedFd<'_>>,
459    ) -> io::Result<()> {
460        let mut data = [status];
461        let mut iov = libc::iovec {
462            iov_base: data.as_mut_ptr().cast(),
463            iov_len: data.len(),
464        };
465        let mut cmsg_buf = CmsgBuf([0; 64]);
466        // SAFETY: an all-zero msghdr is a valid empty message.
467        let mut msg: libc::msghdr = unsafe { std::mem::zeroed() };
468        msg.msg_iov = &mut iov;
469        msg.msg_iovlen = 1;
470        if let Some(fd) = fd {
471            // SAFETY: CMSG_* on a buffer sized and aligned for one fd.
472            unsafe {
473                let space = libc::CMSG_SPACE(std::mem::size_of::<RawFd>() as u32) as usize;
474                debug_assert!(space <= cmsg_buf.0.len());
475                msg.msg_control = cmsg_buf.0.as_mut_ptr().cast();
476                msg.msg_controllen = space as _;
477                let cmsg = libc::CMSG_FIRSTHDR(&msg);
478                (*cmsg).cmsg_level = libc::SOL_SOCKET;
479                (*cmsg).cmsg_type = libc::SCM_RIGHTS;
480                (*cmsg).cmsg_len = libc::CMSG_LEN(std::mem::size_of::<RawFd>() as u32) as _;
481                std::ptr::write_unaligned(libc::CMSG_DATA(cmsg).cast::<RawFd>(), fd.as_raw_fd());
482            }
483        }
484        loop {
485            // SAFETY: msg points at buffers that live until the call returns.
486            if unsafe { libc::sendmsg(sock.as_raw_fd(), &msg, libc::MSG_NOSIGNAL) } >= 0 {
487                return Ok(());
488            }
489            let err = io::Error::last_os_error();
490            if err.kind() != io::ErrorKind::Interrupted {
491                return Err(err);
492            }
493        }
494    }
495
496    /// Receive a status byte and the fd attached to it, if any.
497    pub(super) fn recv_status(sock: BorrowedFd<'_>) -> io::Result<(u8, Option<OwnedFd>)> {
498        let mut data = [0u8; 1];
499        let mut iov = libc::iovec {
500            iov_base: data.as_mut_ptr().cast(),
501            iov_len: data.len(),
502        };
503        let mut cmsg_buf = CmsgBuf([0; 64]);
504        // SAFETY: an all-zero msghdr is a valid empty message.
505        let mut msg: libc::msghdr = unsafe { std::mem::zeroed() };
506        msg.msg_iov = &mut iov;
507        msg.msg_iovlen = 1;
508        msg.msg_control = cmsg_buf.0.as_mut_ptr().cast();
509        msg.msg_controllen = cmsg_buf.0.len() as _;
510        let n = loop {
511            // SAFETY: msg points at buffers that live until the call returns.
512            let n = unsafe { libc::recvmsg(sock.as_raw_fd(), &mut msg, libc::MSG_CMSG_CLOEXEC) };
513            if n >= 0 {
514                break n;
515            }
516            let err = io::Error::last_os_error();
517            if err.kind() != io::ErrorKind::Interrupted {
518                return Err(err);
519            }
520        };
521        if n == 0 {
522            return Err(io::Error::new(
523                io::ErrorKind::UnexpectedEof,
524                "machine connector exited",
525            ));
526        }
527        let mut received = None;
528        // SAFETY: walking the control messages recvmsg filled in.
529        unsafe {
530            let mut cmsg = libc::CMSG_FIRSTHDR(&msg);
531            while !cmsg.is_null() {
532                if (*cmsg).cmsg_level == libc::SOL_SOCKET && (*cmsg).cmsg_type == libc::SCM_RIGHTS {
533                    let fd = std::ptr::read_unaligned(libc::CMSG_DATA(cmsg).cast::<RawFd>());
534                    received = Some(OwnedFd::from_raw_fd(fd));
535                }
536                cmsg = libc::CMSG_NXTHDR(&msg, cmsg);
537            }
538        }
539        if msg.msg_flags & libc::MSG_CTRUNC != 0 {
540            return Err(io::Error::other("truncated control message from connector"));
541        }
542        Ok((data[0], received))
543    }
544}
545
546#[cfg(not(target_os = "linux"))]
547mod sys {
548    use std::io;
549    use std::os::fd::OwnedFd;
550    use std::time::Duration;
551
552    use super::{MachineConnector, MachineConnectorError, MachineSocket};
553
554    pub(super) fn spawn(
555        _leader_pid: u32,
556        _timeout: Duration,
557    ) -> Result<MachineConnector, MachineConnectorError> {
558        Err(MachineConnectorError::Unsupported)
559    }
560
561    pub(super) fn request(
562        _control: &OwnedFd,
563        _socket: MachineSocket,
564    ) -> io::Result<std::os::unix::net::UnixStream> {
565        Err(io::Error::from(io::ErrorKind::Unsupported))
566    }
567
568    pub(super) fn helper_run(_args: &[String]) -> io::Result<()> {
569        Err(io::Error::from(io::ErrorKind::Unsupported))
570    }
571}
572
573#[cfg(all(test, target_os = "linux"))]
574mod tests {
575    use std::io::{Read, Write};
576    use std::os::fd::AsFd;
577    use std::os::unix::net::UnixListener;
578
579    use super::sys::{recv_status, send_status, seqpacket_pair, serve};
580    use super::*;
581
582    #[test]
583    fn test_machine_socket_byte_roundtrip() {
584        for socket in MachineSocket::ALL {
585            assert_eq!(MachineSocket::from_byte(socket as u8), Some(socket));
586        }
587        assert_eq!(MachineSocket::from_byte(0), None);
588        assert_eq!(MachineSocket::from_byte(42), None);
589    }
590
591    #[test]
592    fn test_machine_socket_paths() {
593        assert_eq!(
594            MachineSocket::Manager.path(),
595            "/run/systemd/io.systemd.Manager"
596        );
597        assert_eq!(
598            MachineSocket::Metrics.path(),
599            "/run/systemd/report/io.systemd.Manager"
600        );
601        assert_eq!(
602            MachineSocket::Network.path(),
603            "/run/systemd/netif/io.systemd.Network"
604        );
605    }
606
607    #[test]
608    fn test_status_roundtrip_with_and_without_fd() {
609        let (a, b) = seqpacket_pair().unwrap();
610        send_status(a.as_fd(), 0, None).unwrap();
611        let (status, fd) = recv_status(b.as_fd()).unwrap();
612        assert_eq!((status, fd.is_some()), (0, false));
613
614        let (mut x, mut y) = std::os::unix::net::UnixStream::pair().unwrap();
615        send_status(a.as_fd(), 0, Some(x.as_fd())).unwrap();
616        let (status, fd) = recv_status(b.as_fd()).unwrap();
617        assert_eq!(status, 0);
618        // The passed fd is the same socket: writing to it reaches y.
619        let mut passed = std::os::unix::net::UnixStream::from(fd.unwrap());
620        passed.write_all(b"hi").unwrap();
621        let mut buf = [0u8; 2];
622        y.read_exact(&mut buf).unwrap();
623        assert_eq!(&buf, b"hi");
624        x.flush().unwrap();
625
626        send_status(a.as_fd(), libc::ENOENT as u8, None).unwrap();
627        assert_eq!(recv_status(b.as_fd()).unwrap().0, libc::ENOENT as u8);
628
629        drop(a);
630        let err = recv_status(b.as_fd()).unwrap_err();
631        assert_eq!(err.kind(), std::io::ErrorKind::UnexpectedEof);
632    }
633
634    /// The helper's request loop, against a temp dir standing in for a
635    /// machine's root: allowlisted sockets connect, a missing one reports
636    /// ENOENT, unknown requests EINVAL, and EOF ends the loop cleanly.
637    #[test]
638    fn test_serve_connects_below_root() {
639        let root = tempfile::tempdir().unwrap();
640        let manager = root.path().join("run/systemd/io.systemd.Manager");
641        std::fs::create_dir_all(manager.parent().unwrap()).unwrap();
642        let listener = UnixListener::bind(&manager).unwrap();
643        let root_fd: OwnedFd = std::fs::File::open(root.path()).unwrap().into();
644
645        let (control, helper_end) = seqpacket_pair().unwrap();
646        let server = std::thread::spawn(move || serve(helper_end.as_fd(), root_fd.as_fd()));
647
648        let stream = sys::request(&control, MachineSocket::Manager).unwrap();
649        let (mut accepted, _) = listener.accept().unwrap();
650        (&stream).write_all(b"ok").unwrap();
651        let mut buf = [0u8; 2];
652        accepted.read_exact(&mut buf).unwrap();
653        assert_eq!(&buf, b"ok");
654
655        let err = sys::request(&control, MachineSocket::Network).unwrap_err();
656        assert_eq!(err.raw_os_error(), Some(libc::ENOENT));
657
658        // SAFETY: plain send on a valid fd with a 1 byte buffer.
659        let bogus = [99u8];
660        assert_eq!(
661            unsafe {
662                libc::send(
663                    std::os::fd::AsRawFd::as_raw_fd(&control),
664                    bogus.as_ptr().cast(),
665                    1,
666                    0,
667                )
668            },
669            1
670        );
671        assert_eq!(recv_status(control.as_fd()).unwrap().0, libc::EINVAL as u8);
672
673        drop(control);
674        server.join().unwrap().unwrap();
675    }
676}