1use 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
34pub const HELPER_ARG: &str = "__monitord-machine-connector";
38
39#[derive(Clone, Copy, Debug, PartialEq, Eq)]
44#[repr(u8)]
45pub enum MachineSocket {
46 Manager = 1,
48 Metrics = 2,
50 Network = 3,
52}
53
54impl MachineSocket {
55 pub const ALL: [MachineSocket; 3] = [Self::Manager, Self::Metrics, Self::Network];
56
57 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 #[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
86pub 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 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 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 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 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 let child = self.child.get_mut().unwrap_or_else(|e| e.into_inner());
160 let _ = child.kill();
161 let _ = child.wait();
162 }
163}
164
165pub 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
177pub 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 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 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 let child = std::thread::scope(|scope| {
225 scope
226 .spawn(|| -> io::Result<std::process::Child> {
227 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 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 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 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 pub(super) fn serve(control: BorrowedFd<'_>, root: BorrowedFd<'_>) -> io::Result<()> {
306 loop {
307 let mut byte = [0u8; 1];
308 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 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 fn drop_privileges() -> io::Result<()> {
349 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 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 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 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 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 #[repr(C, align(8))]
452 struct CmsgBuf([u8; 64]);
453
454 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 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 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 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 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 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 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 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 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 #[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 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}