336 views
<center> # 🧦 What Exactly Are Unix Domain Sockets? *Originally published 2026-04-23 by Nick Sweeting on [docs.sweeting.me/s/blog](https://docs.sweeting.me/s/blog).* `/tmp/what_the_heck_is_this.sock` **Contrary to common belief, sockets are not just pipes for strings, they have lots of other powers on Unix systems.** <small>They can move bytes, identities, and even live kernel objects between processes.</small> </center> --- Most programmers first encounter sockets through TCP, and come away with a very simple mental model: open a connection, send some text, receive some text back. That model is fine for getting started, but it misses a big chunk of what unix sockets can do. On Unix systems, sockets are not just dumb string tubes. They are a general-purpose IPC and networking primitive with a bunch of special powers layered on top. Some of those powers are obscure. Some are ancient. Some are quietly running half of your operating system right now. <img src="https://docs.monadical.com/uploads/859bffed-0835-4464-bb7c-292ca211d5e4.png" width="150px" style="float:right"/> In particular, [Unix domain sockets (UDS)](https://man7.org/linux/man-pages/man7/unix.7.html) can do things a normal pipe cannot do at all, like: - identify the process on the other end - transfer open file descriptors between processes, even when owned by other users - act as a local RPC/control channel between privileged and unprivileged components - carry out-of-band control options for the kernel alongside normal payload data That last one is the part that tends to surprise people the most. > In the process of contributing [`/dev/diskN` block-device mounting support](https://github.com/lima-vm/lima/pull/4866) to Lima, I did a deep-dive and finally learned how Unix Sockets actually work. ![](https://docs.monadical.com/uploads/232f415b-45a5-4fbb-8d51-c2a37182001c.png) --- ## Table of Contents [TOC] ## The term "Socket" can refer to several different things At the highest level, a "socket" is just one endpoint of a communication channel. That channel might be: - local to one machine - remote over a network - stream-oriented - message-oriented - connectionless - connection-based A unix "pipe" is much narrower as an abstraction. It is mostly just a one-way byte stream. ![](https://docs.monadical.com/uploads/48c902ff-9c29-484f-85f9-cf76c8a319ef.png) Depending on the socket family and type, sockets can support: - byte streams ([`SOCK_STREAM`](https://man7.org/linux/man-pages/man2/socket.2.html)) - datagrams ([`SOCK_DGRAM`](https://man7.org/linux/man-pages/man2/socket.2.html)) - sequencing and record boundaries - addressing and routing - connection establishment - ancillary metadata That is why TCP sockets, UDP sockets, and [Unix domain sockets](https://man7.org/linux/man-pages/man7/unix.7.html) all feel related, but behave differently. They share a common API shape because they are all sockets, not because they all represent the same kind of transport. A "[unix domain socket](https://man7.org/linux/man-pages/man7/unix.7.html)", what this article is about, is specifically: "a kernel-managed communication endpoint with optional addressing, connection state, metadata, and OS-specific capabilities." <br/> ## Unix Sockets Have Extra Powers Because Unix domain sockets are local to one host, the kernel can do a lot more with them than it can with an arbitrary network socket. For example, Unix sockets can: - live at a filesystem path like `/var/run/my-service.sock` or `/tmp/someservice.sock` (WARNING: path *must* be shorter than ~90 chars due to kernel path length limits) - be protected by normal filesystem permissions - expose [peer credentials](https://man7.org/linux/man-pages/man7/unix.7.html) to the receiver - carry control messages in addition to normal data - transfer open file descriptors with [`SCM_RIGHTS`](https://man7.org/linux/man-pages/man7/unix.7.html) This makes them ideal for local IPC between components that trust the kernel, but do not necessarily trust each other. You can see the “socket file in the filesystem” part directly with `nc` on most Unix systems: ```bash # terminal 1 nc -lU /tmp/demo.sock # terminal 2 printf 'hello over a unix socket\n' | nc -U /tmp/demo.sock # terminal 3 ls -l /tmp/demo.sock ``` That last `ls -l` is one of the first hints that Unix sockets are different from TCP sockets: the endpoint is a real filesystem object, so ownership and permissions matter. You can also see the process side of it with `lsof`: ```bash lsof -U | grep /tmp/demo.sock ``` That will show which process has `/tmp/demo.sock` open, and whether it is listening or connected. <br/> ### 📄 File Descriptors On Unix, if one process has already opened a file, socket, device, pipe, or similar kernel object, it can pass that *open handle* to another process over a [Unix domain socket](https://man7.org/linux/man-pages/man7/unix.7.html). ![](https://docs.monadical.com/uploads/8bfe3068-9c94-4894-827c-09e7fd009411.png) That process on the receiving end does not just get a string path like `/dev/disk4`, it gets the *live open file handle*, so it does not need to reopen the file itself. The sender asks the kernel to attach the open file descriptor to a socket message using ancillary data ([`SCM_RIGHTS`](https://man7.org/linux/man-pages/man7/unix.7.html)). The kernel then installs a new file descriptor into the receiving process. You can see the process-local part of the problem in Bash: ```bash # shell 1 exec 5</etc/hosts lsof -a -p $$ -d 5 # shell 2 lsof -a -p $$ -d 5 ``` In the first shell, FD 5 points at `/etc/hosts`. In the second shell, FD 5 usually does not exist at all. That is why you cannot just send the string `"5"` to another process and expect it to work. Bash cannot directly call [`sendmsg()`](https://man7.org/linux/man-pages/man2/sendmsg.2.html) and [`recvmsg()`](https://man7.org/linux/man-pages/man2/recvmsg.2.html) with ancillary data, so for the actual FD transfer mechanism, here is a full C example using a real pathname socket, `/tmp/test.sock`. The receiving process creates the socket, binds it, listens, and accepts one connection: ```c int server = socket(AF_UNIX, SOCK_STREAM, 0); struct sockaddr_un addr = {0}; addr.sun_family = AF_UNIX; strcpy(addr.sun_path, "/tmp/test.sock"); unlink("/tmp/test.sock"); bind(server, (struct sockaddr *)&addr, sizeof(addr)); listen(server, 1); int conn_fd = accept(server, NULL, NULL); ``` The sending process connects to that socket, opens a file, and passes the FD with [`sendmsg()`](https://man7.org/linux/man-pages/man2/sendmsg.2.html): ```c int sock = socket(AF_UNIX, SOCK_STREAM, 0); struct sockaddr_un addr = {0}; addr.sun_family = AF_UNIX; strcpy(addr.sun_path, "/tmp/test.sock"); connect(sock, (struct sockaddr *)&addr, sizeof(addr)); int fd = open("/dev/disk4", O_RDWR); struct msghdr msg = {0}; char cmsgbuf[CMSG_SPACE(sizeof(int))]; msg.msg_control = cmsgbuf; msg.msg_controllen = sizeof(cmsgbuf); struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg); cmsg->cmsg_level = SOL_SOCKET; cmsg->cmsg_type = SCM_RIGHTS; cmsg->cmsg_len = CMSG_LEN(sizeof(int)); memcpy(CMSG_DATA(cmsg), &fd, sizeof(int)); sendmsg(sock, &msg, 0); ``` Then the receiving process calls [`recvmsg()`](https://man7.org/linux/man-pages/man2/recvmsg.2.html) on that accepted connection and extracts the passed descriptor: ```c struct msghdr msg = {0}; char cmsgbuf[CMSG_SPACE(sizeof(int))]; msg.msg_control = cmsgbuf; msg.msg_controllen = sizeof(cmsgbuf); recvmsg(conn_fd, &msg, 0); struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg); int received_fd; memcpy(&received_fd, CMSG_DATA(cmsg), sizeof(int)); ``` Notice what got transferred there: not the string `"5"`, and not even necessarily the same numeric FD value. The receiver gets its own fresh descriptor number, pointing at the same already-open kernel object. This is how a tiny privileged helper can open something as root, then hand that live handle back to a long-running unprivileged process. #### Why Passing an FD Is Not the Same as Passing a Number A file descriptor is only meaningful inside a single process. If process A has an FD `5`, that does **not** mean process B can do anything useful with the string `"5"`. Why not? Because `5` is just an index into process A’s file descriptor table. Process B has its own separate descriptor table. Its FD `5` might refer to something completely different, or nothing at all. Passing an FD over a Unix socket is special because the kernel is involved. The kernel takes the underlying open file description and creates a fresh descriptor entry in the receiving process. The receiver gets a new local FD number for the same underlying open resource. That is a capability transfer, not a text protocol trick. <br/> ### ⛳️ `SCM_RIGHTS` Flags [`SCM_RIGHTS`](https://man7.org/linux/man-pages/man7/unix.7.html) are special flags you set to tell the kernel when you want to send **file descriptors**. In Unix, a lot of kernel resources are represented as file descriptors, so once you can pass an FD, you can pass a lot of different kinds of live kernel objects. <img src="https://docs.monadical.com/uploads/9bc26e8c-a78e-4b25-893b-23ea44761e58.png" style="width: 250px; float: right"/> > On UNIX, **["Everything is a file"](https://en.wikipedia.org/wiki/Everything_is_a_file)**. In practice, [`SCM_RIGHTS`](https://man7.org/linux/man-pages/man7/unix.7.html) can pass: - regular files - directories - block devices - character devices - pipes and FIFOs - Unix sockets - TCP sockets - listening sockets - already-accepted client sockets - Linux-specific FD-backed objects such as [`memfd`](https://man7.org/linux/man-pages/man2/memfd_create.2.html), [`eventfd`](https://man7.org/linux/man-pages/man2/eventfd.2.html), [`signalfd`](https://man7.org/linux/man-pages/man2/signalfd.2.html), [`timerfd`](https://man7.org/linux/man-pages/man2/timerfd_create.2.html), [`pidfd`](https://man7.org/linux/man-pages/man2/pidfd_open.2.html), [`epoll`](https://man7.org/linux/man-pages/man7/epoll.7.html), [`inotify`](https://man7.org/linux/man-pages/man7/inotify.7.html), [`fanotify`](https://man7.org/linux/man-pages/man7/fanotify.7.html), and [`io_uring`](https://man7.org/linux/man-pages/man7/io_uring.7.html) What it does **not** typically send: - pathnames - process IDs - arbitrary heap pointers - raw memory objects that are not represented by an FD - signals > [`SCM_RIGHTS`](https://man7.org/linux/man-pages/man7/unix.7.html) allows you to transfer open file descriptors between processes, and Unix happens to model a lot of useful kernel resources as file descriptors. <br/> ### 👤 Permissions and Ownership Queries Unix sockets can also help answer the question: *who is on the other end?* ![](https://docs.monadical.com/uploads/add6eaf4-cbea-4001-8411-3f252bfd1ead.png) You can get a rough user-space view of that with tools like: ```bash lsof -U | grep /tmp/test.sock ``` That will show you which process owns the socket and often which client/server processes currently have it open. But if you want the answer from inside the program, from the kernel, you need a syscall-level API. Assume the receiving process already did: ```c int conn_fd = accept(server, NULL, NULL); ``` That `conn_fd` is the connected Unix socket you query for [peer credentials](https://man7.org/linux/man-pages/man7/unix.7.html). On macOS and the BSDs, you can ask the kernel for the peer's effective UID and GID with [`getpeereid()`](https://man.openbsd.org/getpeereid.3): ```c uid_t euid; gid_t egid; if (getpeereid(conn_fd, &euid, &egid) == -1) { perror("getpeereid"); exit(1); } printf("peer euid=%d egid=%d\n", (int)euid, (int)egid); ``` On Linux, the equivalent pattern is usually `getsockopt(..., SO_PEERCRED, ...)`, using [`SO_PEERCRED`](https://man7.org/linux/man-pages/man7/unix.7.html): ```c struct ucred cred; socklen_t len = sizeof(cred); if (getsockopt(conn_fd, SOL_SOCKET, SO_PEERCRED, &cred, &len) == -1) { perror("getsockopt"); exit(1); } printf("peer pid=%d uid=%d gid=%d\n", cred.pid, cred.uid, cred.gid); ``` That gives you a much stronger foundation for local trust than checking a username inside a JSON payload. This is why Unix sockets often show up in designs like: - root helper <-> unprivileged daemon - system service <-> local CLI - desktop compositor <-> app client - container runtime <-> shim process The transport itself can carry more trustable context than “some bytes arrived.” ### 🚂 More Flags and Features If you want the more exhaustive answer to “what can sockets do besides pass strings?”, the right place to look is the [`sendmsg()`](https://man7.org/linux/man-pages/man2/sendmsg.2.html) / [`recvmsg()`](https://man7.org/linux/man-pages/man2/recvmsg.2.html) API. Those calls support: - **normal payload bytes** - **addresses** Useful for datagram sockets where the sender/receiver address matters per message. - **flags** Example: [`MSG_PEEK`](https://man7.org/linux/man-pages/man2/recvmsg.2.html), [`MSG_DONTWAIT`](https://man7.org/linux/man-pages/man2/recvmsg.2.html), [`MSG_NOSIGNAL`](https://man7.org/linux/man-pages/man2/sendmsg.2.html), [and more...](https://man7.org/linux/man-pages/man2/sendmsg.2.html) - **ancillary data (control messages)** This is where the unusual kernel-assisted features live. On Unix domain sockets, the most important ancillary/control-message features are: - **[`SCM_RIGHTS`](https://man7.org/linux/man-pages/man7/unix.7.html)** Pass one or more open file descriptors. - **[peer credentials](https://man7.org/linux/man-pages/man7/unix.7.html)** On Linux this is commonly [`SCM_CREDENTIALS`](https://man7.org/linux/man-pages/man7/unix.7.html) / [`SO_PEERCRED`](https://man7.org/linux/man-pages/man7/unix.7.html). On BSD/macOS you usually query peer creds with [`getpeereid()`](https://man.openbsd.org/getpeereid.3) or related local credential APIs. - **security labels on some systems** Example: Linux [`SO_PEERSEC`](https://man7.org/linux/man-pages/man7/socket.7.html) for SELinux-style peer security context queries. On non-Unix sockets, the kernel can also attach other control information that is not “just strings”, for example: - packet timestamps ([`SO_TIMESTAMP`](https://man7.org/linux/man-pages/man7/socket.7.html), [`SO_TIMESTAMPNS`](https://man7.org/linux/man-pages/man7/socket.7.html), [and more...](https://man7.org/linux/man-pages/man7/socket.7.html)) - destination-address info ([`IP_PKTINFO`](https://man7.org/linux/man-pages/man7/ip.7.html), [`IPV6_PKTINFO`](https://man7.org/linux/man-pages/man7/ipv6.7.html)) - interface indexes ([see `ipi_ifindex` in `IP_PKTINFO`](https://man7.org/linux/man-pages/man7/ip.7.html), [and more...](https://man7.org/linux/man-pages/man7/ipv6.7.html)) - hop limit / TTL metadata ([`IP_RECVTTL`](https://man7.org/linux/man-pages/man7/ip.7.html), [`IPV6_HOPLIMIT`](https://man7.org/linux/man-pages/man7/ipv6.7.html), [and more...](https://man7.org/linux/man-pages/man7/ip.7.html)) - queue overflow indicators ([`SO_RXQ_OVFL`](https://man7.org/linux/man-pages/man7/socket.7.html)) - extended error records ([`IP_RECVERR`](https://man7.org/linux/man-pages/man7/ip.7.html), [`IPV6_RECVERR`](https://man7.org/linux/man-pages/man7/ipv6.7.html), [and more...](https://man7.org/linux/man-pages/man7/ip.7.html)) ## 🚦 Signals Are Related, But Separate Sockets do **not** normally “send signals” in the same way they send bytes or FDs. Signals are a separate kernel mechanism. If process A does: ```bash kill -TERM "$pid" ``` that is not traveling “through” a socket. The kernel is delivering a signal directly to the target process. There are a few ways signals and sockets interact, though: - **[`SIGPIPE`](https://man7.org/linux/man-pages/man7/signal.7.html)**: if you write to a socket whose peer has gone away, the kernel may send your process `SIGPIPE` - **[`SIGIO`](https://man7.org/linux/man-pages/man7/signal.7.html) / async I/O signals**: some socket setups can request signal-based readiness notification - **[`signalfd`](https://man7.org/linux/man-pages/man2/signalfd.2.html) on Linux**: signals can be turned into a readable file descriptor, so you can handle them in an event loop alongside sockets For example, Linux's [`signalfd`](https://man7.org/linux/man-pages/man2/signalfd.2.html) lets you read signals like normal records from an FD: ```c sigset_t mask; sigemptyset(&mask); sigaddset(&mask, SIGTERM); sigprocmask(SIG_BLOCK, &mask, NULL); int sfd = signalfd(-1, &mask, 0); struct signalfd_siginfo info; read(sfd, &info, sizeof(info)); printf("got signal %d\n", info.ssi_signo); ``` That is related to sockets in spirit because it lets you handle “kernel events as file descriptors”, but it is not the same thing as sending a signal over a Unix socket. --- ## 🔐 Security Implications This power cuts both ways. A system that can pass open kernel objects between processes is flexible, but it also means you have to think carefully about authority boundaries. A few rules of thumb: - do not assume “only my binary will call this” is a meaningful security boundary - if a helper runs under `sudo`, make sure the sudoers rule is tightly scoped - prefer verifying [peer credentials](https://man7.org/linux/man-pages/man7/unix.7.html) using the kernel, not just user-controlled request data - put local sockets inside private directories with restrictive permissions - mark sensitive descriptors [`CLOEXEC`](https://man7.org/linux/man-pages/man2/open.2.html) so they do not leak into child processes after `exec` The right mental model is not “this is just a pipe for strings.” It is “this is a capability transport mediated by the kernel.” That is much closer to what is actually happening. ## My 2 Cents Sockets are one of those APIs that seem boring right up until you realize they are carrying half the Unix world on their back. If you only ever use them as a way to send JSON over TCP, you miss most of the interesting part. Unix sockets in particular are one of the cleanest examples of the Unix design philosophy paying off decades later. Once you represent resources as file descriptors, and once you have a local transport that can move those descriptors safely between processes, a lot of elegant system designs suddenly become possible. So yes, sockets can carry strings, but they can do so much more too... --- --- ## Real-World Software That Uses Unix Sockets Once you know this exists, you start seeing it everywhere. A few examples: - [**Wayland**](https://wayland.freedesktop.org/docs/html/ch04.html) uses FD passing for shared memory buffers and GPU-related buffer handles. - [**D-Bus**](https://dbus.freedesktop.org/doc/dbus-specification.html) supports sending Unix file descriptors in messages. - **PipeWire** uses FD passing for media buffers and related resources. - **QEMU / libvirt / virtualization stacks/ [lima](https://github.com/lima-vm/lima/pull/4866)** use passed FDs for privileged devices and networking plumbing. - **systemd** popularized socket activation and the more general pattern of inheriting or handing off already-open descriptors. - **graceful-reload server designs** often transfer listening sockets so a new process can take over without dropping connections. There is a lot of Unix software that would be dramatically more awkward without this feature. --- ### Further reading - ⭐️ https://docs.sweeting.me/s/an-intro-to-the-opt-directory - ⭐️ https://jvns.ca/blog/2016/06/13/should-you-be-scared-of-signals/ - ⭐️ https://kernel-internals.org/ipc/unix-sockets/ - ⭐️ https://froghat.ca/2019/05/scm-rights/ - https://blog.cloudflare.com/know-your-scm_rights/ - https://man7.org/linux/man-pages/man3/cmsg.3.html - https://docs.sweeting.me/s/system-monitoring-tools - https://liujunming.top/2024/07/14/File-Descriptor-Transfer-over-Unix-Domain-Sockets/ - [SCM_RIGHTS API Quirks](https://gist.github.com/kentonv/bc7592af98c68ba2738f4436920868dc) - [`man 2 sendmsg`](https://man7.org/linux/man-pages/man2/sendmsg.2.html) + [`man 2 recvmsg`](https://man7.org/linux/man-pages/man2/recvmsg.2.html) + [`man 7 unix`](https://man7.org/linux/man-pages/man7/unix.7.html) - https://lwn.net/Articles/1023085/ - https://elixir.bootlin.com/linux/v5.17.3/source/include/linux/socket.h#L163 - https://linuxvox.com/blog/can-i-open-a-socket-and-pass-it-to-another-process-in-linux/ - https://blogs.oracle.com/linux/unix-garbage-collection-and-iouring - https://inspektor-gadget.io/docs/latest/gadgets/fdpass/