QUESTION
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
Structure diagram
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
Call path
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.
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.
SOURCE COORDINATES
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.
| File | Function / structure | What 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 PROGRAM
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.
cc -std=c17 -Wall -Wextra -O2 send_fd.c -o send_fd01#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
Code notes
SOCK_SEQPACKET | SOCK_CLOEXECCreates a local socket pair that preserves message boundaries and prevents exec inheritance on both endpoints.
CMSG_SPACE(sizeof(int))Calculates control-buffer size including alignment padding for cmsghdr and payload. CMSG_LEN serves a different purpose.
SCM_RIGHTSThe kernel transfers struct file references for the fds rather than simply copying the int payload unchanged to the receiver.
MSG_CMSG_CLOEXECAtomically sets close-on-exec on every installed received fd, avoiding a leakage race between recvmsg and fcntl.
CMSG_FIRSTHDR(&in)Production code must walk every control message with CMSG_NXTHDR and validate MSG_CTRUNC, length, and the expected fd count.
DETAILS
Detailed behavior
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.
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.
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
Objects and lifetimes
| Object | Creation and release | Values to inspect |
|---|---|---|
scm_fp_list | Collected by sendmsg and retained as file refs for the lifetime of the socket message | count, struct file array |
cmsghdr | Exists in the user control buffer during the send/recv syscall | level, type, length |
received fd | Installed in the receiver fd table by recvmsg and released by close | CLOEXEC, shared f_pos/f_flags |
FAILURE PATH
Failure conditions and common misconceptions
| Observed symptom | Likely causes | How to verify |
|---|---|---|
| recv fails with EMFILE | The receiver has insufficient fd capacity | Check RLIMIT_NOFILE and the limit on received descriptors |
| An unexpected fd leaks | MSG_CTRUNC/validation/error cleanup was omitted | Inspect /proc/PID/fd and the close path |
| The read offset moves | The sender shares the open file description | Inspect fdinfo pos and protocol intent |
LAB
Verify it yourself
- Verify that the receiver can still read immediately after the sender closes the source.
- Alternate one-byte reads through the received and source fds to confirm the shared file offset.
- Add SCM_CREDENTIALS and compare sender pid/uid/gid with the SO_PEERCRED result.
./send_fdstrace -e trace=socketpair,openat,sendmsg,recvmsg,read,close ./send_fdPRIMARY REFERENCES