Signal / Event / IPC · Linux userspace / kernel ABI

Unix sockets and SCM_RIGHTS

Explains, control message by control message, how a Unix domain socket installs not just bytes but an open file reference in another process's fd table.

Series
25 / 38
Build
cc -std=c17 -Wall -Wextra -O2 send_fd.c -o send_fd
Run
./send_fd
Kernel
Linux 6.18.37 LTS

Can processes share the same file by sending an fd number?

Sending the integer 7 from process A does not give it the same meaning as slot 7 in process B's fd table. With SCM_RIGHTS, the kernel turns the source fd into a struct file reference and installs it in a new fd slot at the receiver, transferring the same open file description.

On a stream Unix socket, it is safest to send at least one byte of normal data with the control message. The receiver must validate cmsghdr level, type, and length, and immediately close unexpected fds.

Structure diagram

Figure 1. The file reference transferred by SCM_RIGHTS

Sender fd table

  • fd 5
  • FD_CLOEXEC
  • sendmsg control

Socket message

  • SCM_RIGHTS
  • scm_fp_list
  • struct file ref +1

Shared struct file

  • f_pos=12
  • O_RDONLY
  • inode /etc/hostname

Receiver fd table

  • new fd 8
  • MSG_CMSG_CLOEXEC
  • same f_pos/f_flags

The sender's fd number 5 is not copied unchanged to receiver fd number 8. Both numbers are made to refer to the same struct file.

Call path

Figure 2. From userspace code to observable results
sender fd struct file lookup
sendmsg cmsghdr SCM_RIGHTS
socket queue retain file reference
recvmsg receiver fd install
shared file share offset/status flags

What is transferred is an open-file-description reference, not a number. The receiver gets a newly allocated number, but can share the file offset and status flags with the sender.

Figure 3. Major points along the kernel-internal path
scm_send validate fd array
unix_scm_to_skb file refs attach
socket queue skb lifetime
scm_recv new fd reserve
fd_install receiver table

This diagram is not for memorizing function names. Follow which return value, file descriptor, memory mapping, or wait queue is passed to the next stage.

Linux 6.18.37 LTS source locations

Go beyond the glibc function to the files where the syscall implementation meets kernel objects. Each link points to the original file at the same tag.

FileFunction / structureWhat to inspect
net/core/scm.c scm_fp_copy(), scm_detach_fds() Copy fds into an array of file references and install them at the receiver
net/unix/af_unix.c unix_stream_sendmsg(), unix_stream_read_generic() Deliver the control message with the Unix socket queue
fs/file.c get_unused_fd_flags(), fd_install() Allocate receiver fd slots

Complete runnable example

The code below is not pseudocode with explanatory lines omitted. It is a minimal example that can be built and run as a file.

Buildcc -std=c17 -Wall -Wextra -O2 send_fd.c -o send_fd
01#define _GNU_SOURCE
02#include <fcntl.h>
03#include <stdio.h>
04#include <string.h>
05#include <sys/socket.h>
06#include <unistd.h>
07
08int main(void)
09{
10    int pair[2];
11    if (socketpair(AF_UNIX, SOCK_SEQPACKET | SOCK_CLOEXEC, 0, pair) < 0)
12        return 1;
13    int source = open("/etc/hostname", O_RDONLY | O_CLOEXEC);
14    if (source < 0)
15        return 1;
16
17    char marker = 'F';
18    struct iovec iov = { .iov_base = &marker, .iov_len = 1 };
19    char control[CMSG_SPACE(sizeof(int))];
20    memset(control, 0, sizeof(control));
21    struct msghdr out = { .msg_iov = &iov, .msg_iovlen = 1,
22                          .msg_control = control, .msg_controllen = sizeof(control) };
23    struct cmsghdr *cmsg = CMSG_FIRSTHDR(&out);
24    cmsg->cmsg_level = SOL_SOCKET;
25    cmsg->cmsg_type = SCM_RIGHTS;
26    cmsg->cmsg_len = CMSG_LEN(sizeof(int));
27    memcpy(CMSG_DATA(cmsg), &source, sizeof(source));
28    if (sendmsg(pair[0], &out, 0) < 0)
29        return 1;
30
31    char in_control[CMSG_SPACE(sizeof(int))];
32    struct msghdr in = { .msg_iov = &iov, .msg_iovlen = 1,
33                         .msg_control = in_control, .msg_controllen = sizeof(in_control) };
34    if (recvmsg(pair[1], &in, MSG_CMSG_CLOEXEC) < 0)
35        return 1;
36    int received = -1;
37    cmsg = CMSG_FIRSTHDR(&in);
38    if (cmsg && cmsg->cmsg_level == SOL_SOCKET && cmsg->cmsg_type == SCM_RIGHTS)
39        memcpy(&received, CMSG_DATA(cmsg), sizeof(received));
40    char text[128];
41    ssize_t n = read(received, text, sizeof(text));
42    if (n > 0)
43        write(STDOUT_FILENO, text, (size_t)n);
44    close(received); close(source); close(pair[0]); close(pair[1]);
45    return n < 0;
46}

Code notes

Source line 11SOCK_SEQPACKET | SOCK_CLOEXEC

Creates a local socket pair that preserves message boundaries and prevents exec inheritance on both endpoints.

Source line 19CMSG_SPACE(sizeof(int))

Calculates control-buffer size including alignment padding for cmsghdr and payload. CMSG_LEN serves a different purpose.

Source line 25SCM_RIGHTS

The kernel transfers struct file references for the fds rather than simply copying the int payload unchanged to the receiver.

Source line 34MSG_CMSG_CLOEXEC

Atomically sets close-on-exec on every installed received fd, avoiding a leakage race between recvmsg and fcntl.

Source line 37CMSG_FIRSTHDR(&in)

Production code must walk every control message with CMSG_NXTHDR and validate MSG_CTRUNC, length, and the expected fd count.

Detailed behavior

01

Control-buffer truncation can leak resources

A small buffer can set MSG_CTRUNC. Check whether some transferred fds were installed and the relevant kernel rules; on a protocol violation, close every fd that was received.

Without a maximum receive count, a peer can exhaust the fd-table limit.

02

Credential delivery can be combined with it

SO_PASSCRED/SCM_CREDENTIALS or SO_PEERCRED can verify the local peer's PID/UID/GID. Pathname-socket permissions do not replace all authorization for a long-lived connection.

Define the connection-time authorization model with PID namespaces and possible credential changes in mind.

03

Confirm whether a shared offset is intended

Like dup, an fd received with SCM_RIGHTS refers to the same open file description, so the read offset and O_NONBLOCK/O_APPEND are shared. If an independent offset is required, the receiver must perform a separate open from a pathname or handle.

If the sender closes after transfer, the receiver's reference remains and keeps the file object alive.

Objects and lifetimes

ObjectCreation and releaseValues to inspect
scm_fp_listCollected by sendmsg and retained as file refs for the lifetime of the socket messagecount, struct file array
cmsghdrExists in the user control buffer during the send/recv syscalllevel, type, length
received fdInstalled in the receiver fd table by recvmsg and released by closeCLOEXEC, shared f_pos/f_flags

Failure conditions and common misconceptions

Observed symptomLikely causesHow to verify
recv fails with EMFILEThe receiver has insufficient fd capacityCheck RLIMIT_NOFILE and the limit on received descriptors
An unexpected fd leaksMSG_CTRUNC/validation/error cleanup was omittedInspect /proc/PID/fd and the close path
The read offset movesThe sender shares the open file descriptionInspect fdinfo pos and protocol intent

Verify it yourself

  1. Verify that the receiver can still read immediately after the sender closes the source.
  2. Alternate one-byte reads through the received and source fds to confirm the shared file offset.
  3. Add SCM_CREDENTIALS and compare sender pid/uid/gid with the SO_PEERCRED result.
Run./send_fd
Tracestrace -e trace=socketpair,openat,sendmsg,recvmsg,read,close ./send_fd

Primary sources