SCM_RIGHTS: Passing File Descriptors Between Unrelated Processes

2026-07-20

A file descriptor is just a small integer — an index into your process's file descriptor table. So handing "fd 7" to another process is meaningless: their fd 7 points somewhere else entirely. What you actually need to transfer is the underlying struct file the kernel holds, and get a new index into the receiver's table pointing at it.

Unix domain sockets support exactly this via the SCM_RIGHTS ancillary message, sent alongside a normal payload with sendmsg()/recvmsg(). The sender packs an array of fds into a cmsghdr; the kernel walks each one, bumps the refcount on its struct file, and — when the receiver calls recvmsg() — allocates fresh fd numbers in the receiver's table pointing at those same file objects. The receiver's fd numbers won't match the sender's, and that's the point.

You still need a dummy byte of real payload: msg_iov must be non-empty or the message may be silently dropped. And the receiver must provide a msg_control buffer big enough via CMSG_SPACE(sizeof(int) * n) — if it's too small, the kernel truncates the fd array and sets MSG_CTRUNC, leaking the descriptors (they get closed in the sender but never appear in the receiver).

Real-world example: Chrome's sandbox uses this constantly. The renderer process runs with almost no filesystem access. When it needs to read a font file, it asks the privileged broker process over a Unix socket. The broker opens the file, then passes the fd back via SCM_RIGHTS. The renderer now has a working fd for a file it could never have opened itself. Same trick powers systemd socket activation (init opens the listening socket, hands it to your daemon on exec) and Wayland (passing dmabuf handles for zero-copy GPU buffers).

Rule of thumb for buffer sizing: To pass N descriptors, allocate CMSG_SPACE(N * sizeof(int)) bytes for msg_control. On x86-64 with N=1, that's 24 bytes (16-byte cmsghdr + 4-byte int + 4 bytes padding to 8-byte alignment). Always check msg.msg_flags & MSG_CTRUNC after receiving — a truncated fd is a leaked kernel object.

Watch out for: The receiver's fds count against their RLIMIT_NOFILE, not the sender's. A malicious peer can exhaust your fd table by spamming SCM_RIGHTS messages. And fds sent but never received (peer never calls recvmsg before closing) are closed by the kernel automatically when the socket dies — no leak, but no delivery either.

Key Takeaway: SCM_RIGHTS transfers the underlying kernel file object, not the fd number — the receiver gets a fresh index into their own descriptor table pointing at the same open file, enabling privilege-separated architectures like Chrome's sandbox and systemd socket activation.

All newsletters