Systems Programming · OS Interfaces
UNIX Domain Sockets and Passing File Descriptors
A socket in the filesystem namespace carries data without a network stack, and its control-message channel can hand a descriptor to another process. Address formats, the credential channel, and what a descriptor transfer actually copies.
A UNIX domain socket is an address family with no network beneath it. AF_UNIX endpoints are connected by the kernel directly, and the data path never leaves the machine, never enters the IP stack, and never consults a routing table. What remains is the socket interface: a byte stream or a datagram sequence, a receive queue, and an ancillary channel that can carry things ordinary sockets cannot. The ancillary channel is the reason the family still matters. It is the only portable way to transfer an open file descriptor between processes.
Two namespaces, one address structure
The address is a sockaddr_un, which begins with a family field and a 108-byte path buffer on Linux. The size of that buffer is the binding constraint on the whole naming scheme: a path longer than 107 bytes plus a terminating null does not fit, and bind fails with ENAMETOOLONG. Other systems use different lengths, and POSIX does not fix one, so portable code must either stay well under the smallest common limit or discover it.
A pathname socket is bound to a filesystem entry. bind creates the socket inode at that path, and the entry persists until the socket is closed and the path is unlinked; closing the descriptor alone leaves the path behind, and a later bind on the same path fails with EADDRINUSE. The entry is a socket, not a regular file, so reading it is meaningless, but its permissions govern who may connect. connect checks write permission on the socket inode, which is why a socket in a directory with mode 0700 is reachable only by processes that can traverse the directory and write the node.
The abstract namespace, on Linux only, is signalled by a leading null byte in the path field. The name has no filesystem representation, no permissions, and no cleanup requirement: it disappears when the last descriptor referring to it closes. Two processes that agree on a name can rendezvous without any directory existing, which suits container runtimes and transient services. The trade-off is that there is nothing to inspect: a tool listing the filesystem shows nothing, and access control must be enforced by the protocol rather than by the filesystem.
The path lookup is not a security boundary by itself. The check happens at connect time, on the path as resolved then, and a socket that is unlinked and rebound elsewhere changes what the name refers to. A service that needs to know its peer rather than its peer’s name should use the credential channel described below.
Three socket types, three delivery models
SOCK_STREAM provides a byte stream with the same semantics as a TCP socket: no message boundaries, a connection established by connect and accept, and end-of-file when the peer closes. It has no retransmission, no congestion control, and no segmentation by the network, so the only short-count sources are the buffer size and the receiver’s read size.
SOCK_DGRAM preserves message boundaries. Each send produces one datagram, and each recv returns one datagram or truncates it to the buffer provided, discarding the remainder. Unlike UDP there is no possibility of loss through the network, but the send queue is bounded: a datagram sent to a peer whose queue is full is dropped or blocks, depending on whether the socket is non-blocking. The default maximum datagram size is governed by the socket send buffer, and the kernel refuses sends above it with EMSGSIZE.
SOCK_SEQPACKET is the middle case: connection-oriented like a stream, but preserving boundaries like a datagram, with a maximum record size and no truncation loss because the receiver can query the record length. It is the natural fit for request-response protocols where a request is one record, and it is available on Linux and the BSDs but not universally.
socketpair creates two connected endpoints in one call, with no filesystem name and no connection step. It is the standard replacement for a pipe when the communication is bidirectional, and it is how a process gives a child a private channel that no other process can reach.
The control-message channel
The data channel carries bytes. The control channel carries metadata, attached to a specific message and delivered with it or not at all. sendmsg takes an array of struct cmsghdr describing the ancillary data; recvmsg returns the control data in a buffer alongside the payload.
#include <sys/socket.h>
#include <string.h>
#include <unistd.h>
/* Send one descriptor plus a one-byte payload over a socket. */
int send_fd(int sock, int fd) {
char payload = 'F';
struct iovec iov = { .iov_base = &payload, .iov_len = 1 };
char cbuf[CMSG_SPACE(sizeof(int))];
memset(cbuf, 0, sizeof cbuf);
struct msghdr msg = { 0 };
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
msg.msg_control = cbuf;
msg.msg_controllen = sizeof cbuf;
struct cmsghdr *c = CMSG_FIRSTHDR(&msg);
c->cmsg_level = SOL_SOCKET;
c->cmsg_type = SCM_RIGHTS;
c->cmsg_len = CMSG_LEN(sizeof(int));
memcpy(CMSG_DATA(c), &fd, sizeof(int));
return sendmsg(sock, &msg, 0) == 1 ? 0 : -1;
}
The alignment is not incidental. CMSG_SPACE and CMSG_LEN exist because the header and payload must be padded to the platform’s alignment for a struct cmsghdr, and computing the buffer size by hand with sizeof produces a buffer that is short by the padding on some architectures. The macros are the portable form and must be used for both allocation and length fields.
On the receiving side the buffer must be large enough for the control data the sender attached. If it is not, the message is truncated, MSG_CTRUNC is set in msg_flags, and the descriptors that did not fit are closed by the kernel — the transfer is not partial in the useful sense. A receiver that ignores MSG_CTRUNC silently loses descriptors and, worse, loses track of which ones it now owns.
MSG_CMSG_CLOEXEC, accepted by recvmsg on Linux, sets FD_CLOEXEC on every descriptor received in that call. Without it, the descriptors arrive without the flag and are inherited by any child spawned before the receiver sets it, which reintroduces the race described in fork, exec, and descriptor inheritance.
What transfer copies
Passing a descriptor does not pass the file. The kernel sends the receiving process a new entry in its own descriptor table, referring to the same open file description. The offset, the status flags, and the access mode are the ones the sender had, because those live on the description, and the description is shared. Two processes reading through transferred descriptors of the same file advance one offset, exactly as if the descriptors had been duplicated.
The reference count is what makes this safe. The description survives until every descriptor referring to it is closed, in any process, so the sender may close its copy immediately after sendmsg and the object remains valid in the receiver. This is the mechanism behind privilege separation designs: a privileged process opens a file, checks the caller, and passes the descriptor to an unprivileged worker, which then reads it without ever holding the right to open it.
Sending a descriptor that refers to a socket is how a listening socket is shared among worker processes. Each worker receives a duplicate referring to the same description, and the kernel distributes incoming connections among the processes blocked in accept on it. The queue is shared, so a connection accepted by one worker is gone from the others, and the accept loop in each must be prepared for EAGAIN when another worker wins the race.
A descriptor passed over a socket is not restricted to files. Pipes, event descriptors, memory objects, and directories can all be transferred, subject to per-object rules. What cannot be transferred is the authority to do something the receiver could not otherwise do: the descriptor carries its own access rights, but a process that receives a read-only descriptor of a file it cannot open has gained only the ability to read that one object.
Credentials and peer identity
The kernel knows the identity of the process on the other end, and on Linux it will report it. Setting SO_PASSCRED on a socket makes the receiver obtain a SCM_CREDENTIALS control message with the sender’s pid, uid, and gid for each message. The values are those at the time of the send, recorded by the kernel, and are not forgeable by the sender.
The same structure can be sent explicitly to assert an identity, but the kernel validates it: a process may only send its own real, effective, or saved ids, and a mismatch causes the message to be rejected or the credentials replaced. This is what allows a set-user-id program to communicate its original identity to a peer that would otherwise only see the elevated one.
SO_PEERCRED takes a different route: a single getsockopt on a connected socket returns the credentials of the peer as recorded at connect time, with no per-message cost. It is the cheaper option for a server that authenticates a client once at connection establishment. Its limitation is that the credentials are fixed at connection time, so a process that changes its identity afterwards is still described by the old values.
Neither mechanism exists in portable POSIX. The BSDs have their own equivalents with different names and structures, and the general pattern — a kernel-provided, unspoofable peer identity attached to the connection — is what the Linux-specific calls implement.
Failure modes that are specific to the family
ECONNREFUSED from connect means the path exists but no process is listening on it. This is a common state after a crash: the socket file is still on disk, the process that bound it is gone, and every client fails until something unlinks the path. A service that binds a pathname socket should unlink the path before binding, after checking that no live process answers on it, because unlinking an active socket leaves the running service unreachable through its name.
EPIPE and SIGPIPE behave as they do for any socket: writing to an endpoint whose peer has closed raises SIGPIPE, which terminates the process by default, and returns EPIPE if the signal is ignored or blocked. The signal is not specific to the domain family, but the failure is more common here because local peers close abruptly when they exit.
A stream socket supports half-close: shutdown with SHUT_WR sends end-of-file to the peer while leaving the read direction open. The pattern is a request-response protocol where the client writes a request, shuts down its write side, and reads the response until end-of-file. Without the half-close the server cannot distinguish the end of the request from a pause, and a protocol that relies on a length prefix instead must handle a truncated prefix as a distinct error.
The receive queue is bounded by SO_RCVBUF, and a sender that fills it blocks or gets EAGAIN. For a datagram socket the same limit causes silent drops rather than backpressure, because there is no retransmission to recover them. A protocol over SOCK_DGRAM in this family must therefore carry its own acknowledgement if it needs reliability, at which point the boundary-preserving property is the only thing it is buying.
For waiting on many of these endpoints at once, the mechanisms in readiness notification apply unchanged; a UNIX socket is a file description like any other, and epoll accepts it. What differs is only the addressing and the ancillary channel.
References
- unix(7) — Linux manual page — man7.org
- sendmsg(2) — Linux manual page — man7.org
- The Open Group Base Specifications Issue 7, 2018 edition — System Interfaces — The Open Group
- Advanced Programming in the UNIX Environment — W. Richard Stevens and Stephen A. Rago, Addison-Wesley, 3rd edition, 2013