A Unix-domain socket can carry ancillary data with a normal payload. SCM_RIGHTS lets the kernel install references to open file descriptions in another process's descriptor table. This is useful when a broker opens a resource and delegates use of that exact handle to a worker.
Share a reference, not an integer
The receiver's number is allocated locally; it need not match the sender's. The shared open file description retains its offset and status flags, while per-descriptor inheritance flags are independent. A successful queued message holds a reference even if the sender closes its original descriptor before recvmsg runs.
#include <cassert>
#include <cstdio>
#include <cstdlib>
#include <fcntl.h>
#include <unistd.h>
#include <cstring>
#include <sys/socket.h>
int main() {
char name[]="/tmp/unix-check-XXXXXX"; int fd=::mkstemp(name); assert(fd>=0); ::unlink(name);
assert(::write(fd,"abcdef",6)==6); assert(::lseek(fd,0,SEEK_SET)==0);
assert(::fcntl(fd,F_SETFD,FD_CLOEXEC)==0);int p[2];assert(::socketpair(AF_UNIX,SOCK_SEQPACKET,0,p)==0);
alignas(cmsghdr) char control[CMSG_SPACE(sizeof(int))]{};char tag='t';iovec io{&tag,1};msghdr msg{};
msg.msg_iov=&io;msg.msg_iovlen=1;msg.msg_control=control;msg.msg_controllen=sizeof control;
auto* h=CMSG_FIRSTHDR(&msg);h->cmsg_level=SOL_SOCKET;h->cmsg_type=SCM_RIGHTS;h->cmsg_len=CMSG_LEN(sizeof(int));std::memcpy(CMSG_DATA(h),&fd,sizeof fd);
assert(::sendmsg(p[0],&msg,MSG_NOSIGNAL)==1);msg.msg_controllen=sizeof control;
assert(::recvmsg(p[1],&msg,MSG_CMSG_CLOEXEC)==1);
int received=-1;std::memcpy(&received,CMSG_DATA(CMSG_FIRSTHDR(&msg)),sizeof received);char byte=0;assert(::read(received,&byte,1)==1);
std::printf("%c %ld %d\n",byte,long(::lseek(fd,0,SEEK_CUR)),bool(::fcntl(received,F_GETFD)&FD_CLOEXEC));::close(received);::close(p[0]);::close(p[1]);::close(fd);
}The example makes the inheritance distinction visible. It does not serialize a C++ owner object. The sending process remains responsible for its original owner, while a successful receiver creates a new owner for its installed handle. A protocol should also decide what kinds of descriptors it accepts and what operations the worker is allowed to perform on them.
Ancillary storage is an ABI boundary
Use CMSG_SPACE for control-buffer capacity, CMSG_LEN for a header's actual length, and the iteration macros to find records. Align the storage for cmsghdr. The build uses SOCK_SEQPACKET to preserve the tag-plus-control record boundary; that is a different framing contract from a TCP or Unix stream socket.
Receive with MSG_CMSG_CLOEXEC so the kernel installs the inheritance flag atomically. The sender's FD_CLOEXEC bit does not travel with its right. A later separate fcntl leaves an avoidable race with another thread's exec.
Validation happens after acquisition
recvmsg can return payload truncation or ancillary truncation, and it may already have installed descriptors before the application discovers the protocol error. Put every installed right under ownership before deciding whether to accept the message. Reject a missing or extra right, a payload of the wrong length, and MSG_TRUNC/MSG_CTRUNC with all received handles closed.
Linux closes excess rights that cannot fit in the control buffer, but rights that did fit are still the receiver's responsibility. Ignoring an extra header is not cleanup. The reference uses a fixed array of move-only owners so rejection unwinds ownership automatically without an allocation between installation and validation.
The tests send real ancillary messages, verify close-on-exec and a shared cursor, check rejection cleanup by counting open descriptors, and transfer a right from a forked child. They also exercise oversized ancillary data and an oversized payload. An empty nonblocking socket reports EAGAIN; a malformed message is a protocol error, not an empty-success result.
Before the fill, trace a message containing two valid rights when the protocol allows one. Which descriptor closes on rejection, and what is still owned by the sender? Then explain why closing the sender's original after sendmsg succeeds does not cancel a queued transfer. You can now apply the sequence in your own small C++ service with explicit ownership, lifecycle and error contracts.