zlayer-agent 0.13.0

Container runtime agent using libcontainer/youki
Documentation
//! `SCM_RIGHTS` file-descriptor passing over a Unix socket — the daemon side of
//! the runc "console-socket" PTY handoff.
//!
//! When the daemon execs an interactive process via the single-threaded
//! `zlayer runtime exec --tty --console-socket <path>` subprocess, libcontainer
//! (inside that subprocess) creates the PTY master/slave pair and sends the
//! **master** fd back to the daemon over a Unix-domain socket using
//! `SCM_RIGHTS` ancillary data (see youki `crates/libcontainer/src/tty.rs`
//! `setup_console`, which `sendmsg`s `ControlMessage::ScmRights(&[master])`
//! with the payload bytes `b"/dev/ptmx"`).
//!
//! [`recv_fd`] is the daemon's receiver: it `recvmsg`s a single fd out of the
//! ancillary `SCM_RIGHTS` control message and returns it as an [`OwnedFd`].
//! [`send_fd`] is the symmetric sender (mirroring tty.rs), kept for the
//! round-trip unit tests and so the handoff protocol lives in one place.
//!
//! Linux + `youki-runtime` only; gated identically to the [`super::youki`]
//! module that consumes it.
#![cfg(all(target_os = "linux", feature = "youki-runtime"))]

use std::io::{IoSlice, IoSliceMut};
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd};
use std::os::unix::net::UnixStream;

use nix::sys::socket::{recvmsg, sendmsg, ControlMessage, ControlMessageOwned, MsgFlags, UnixAddr};

/// Receive a single file descriptor sent over `stream` via `SCM_RIGHTS`.
///
/// The peer (libcontainer's `setup_console`) sends the PTY master fd as
/// ancillary `SCM_RIGHTS` data alongside a small data payload (`b"/dev/ptmx"`).
/// We must supply a **non-empty** data buffer to `recvmsg`: a zero-length
/// iovec causes the kernel to treat the receive as having no data segment and
/// drop the ancillary control message, so the fd never arrives.
///
/// # Errors
///
/// Returns an error if the `recvmsg` syscall fails or if the message carried
/// no `SCM_RIGHTS` descriptor (e.g. the peer died before sending the fd).
#[allow(unsafe_code)]
pub fn recv_fd(stream: &UnixStream) -> std::io::Result<OwnedFd> {
    // Non-empty scratch buffer for the data segment — required or the kernel
    // discards the ancillary fd. The peer sends `b"/dev/ptmx"` (9 bytes).
    let mut scratch = [0u8; 16];
    let mut iov = [IoSliceMut::new(&mut scratch)];
    let mut cmsg_buf = nix::cmsg_space!([RawFd; 1]);

    let msg = recvmsg::<UnixAddr>(
        stream.as_raw_fd(),
        &mut iov,
        Some(&mut cmsg_buf),
        MsgFlags::empty(),
    )
    .map_err(std::io::Error::from)?;

    let cmsgs = msg
        .cmsgs()
        .map_err(|e| std::io::Error::other(format!("recvmsg cmsgs decode failed: {e}")))?;
    for cmsg in cmsgs {
        if let ControlMessageOwned::ScmRights(fds) = cmsg {
            if let Some(&raw) = fds.first() {
                // SAFETY: `raw` is a freshly received fd that the kernel just
                // installed into our process's fd table via SCM_RIGHTS. We
                // take sole ownership of it; the kernel does not retain a
                // duplicate, so wrapping it in `OwnedFd` (which closes on
                // drop) is the correct ownership transfer.
                let owned = unsafe { OwnedFd::from_raw_fd(raw) };
                return Ok(owned);
            }
        }
    }

    Err(std::io::Error::new(
        std::io::ErrorKind::UnexpectedEof,
        "console socket peer sent no SCM_RIGHTS file descriptor",
    ))
}

/// Send a single file descriptor over `stream` via `SCM_RIGHTS`.
///
/// Mirrors libcontainer's `setup_console` sender (youki `tty.rs:305-310`): a
/// one-byte data payload plus a `ControlMessage::ScmRights(&[fd])` ancillary
/// message. The data byte is required so the receiver's non-empty iovec has
/// something to read and the kernel preserves the control message.
///
/// # Errors
///
/// Returns an error if the underlying `sendmsg` syscall fails.
//
// Only exercised by this module's round-trip unit tests today (the production
// daemon is purely the receiver via `recv_fd`); kept for protocol symmetry and
// test coverage, so it is dead in a non-test build.
#[cfg_attr(not(test), allow(dead_code))]
pub fn send_fd(stream: &UnixStream, fd: RawFd) -> std::io::Result<()> {
    let payload = [0u8; 1];
    let iov = [IoSlice::new(&payload)];
    let fds = [fd];
    let cmsg = [ControlMessage::ScmRights(&fds)];
    sendmsg::<UnixAddr>(stream.as_raw_fd(), &iov, &cmsg, MsgFlags::empty(), None)
        .map_err(std::io::Error::from)?;
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::io::{Read, Write};
    use std::os::fd::AsRawFd;

    /// Round-trip a real PTY master fd through `send_fd` → `recv_fd` over a
    /// connected `UnixStream` pair, then assert the received fd drives the
    /// same terminal: a byte written to the PTY slave is readable through the
    /// received master.
    #[test]
    fn send_recv_real_pty_master_is_usable() {
        let pty = nix::pty::openpty(None, None).expect("openpty");
        let (a, b) = UnixStream::pair().expect("UnixStream::pair");

        // Sender hands the master fd across; keep the original master alive in
        // this process until after the send (SCM_RIGHTS dups it into the peer).
        send_fd(&a, pty.master.as_raw_fd()).expect("send_fd");
        let received = recv_fd(&b).expect("recv_fd");

        // Drop our local copy of the master so only the received fd remains on
        // the read side; the slave stays open so the master doesn't EOF.
        drop(pty.master);

        // Write through the slave; the received master must read it back.
        // `File::from(OwnedFd)` is a safe conversion (ownership transfer).
        // We write "ping" (no newline) to avoid the PTY line discipline's
        // ONLCR `\n` -> `\r\n` translation muddying the byte comparison — the
        // point is to prove the received fd is the SAME live terminal, which a
        // clean read-back of the written bytes establishes.
        let mut slave_file = std::fs::File::from(pty.slave);
        slave_file.write_all(b"ping").expect("write to slave");
        slave_file.flush().expect("flush slave");

        let mut master_file = std::fs::File::from(received);
        let mut buf = [0u8; 4];
        master_file.read_exact(&mut buf).expect("read from master");
        assert_eq!(&buf, b"ping");
    }

    /// Round-trip a plain tempfile fd and assert the received fd refers to the
    /// SAME underlying file (identical device + inode via `fstat`), proving
    /// `recv_fd` transfers the real descriptor rather than a fresh open.
    #[test]
    fn send_recv_tempfile_fd_same_inode() {
        use nix::sys::stat::fstat;

        let tmp = tempfile::NamedTempFile::new().expect("tempfile");
        let file = tmp.reopen().expect("reopen");
        let orig = fstat(&file).expect("fstat orig");

        let (a, b) = UnixStream::pair().expect("UnixStream::pair");
        send_fd(&a, file.as_raw_fd()).expect("send_fd");
        let received = recv_fd(&b).expect("recv_fd");

        let recv_stat = fstat(&received).expect("fstat recv");
        assert_eq!(orig.st_dev, recv_stat.st_dev, "device must match");
        assert_eq!(orig.st_ino, recv_stat.st_ino, "inode must match");
    }
}