Unix Domain Socket File Descriptor Inheritance in Forked Processes
0 reputation · 17 Nov 2020, 18:13 UTC
0 reputation · 17 Nov 2020, 18:13 UTC
Unix domain sockets provide efficient inter-process communication through filesystem-based addressing, where the socket file serves as both address and endpoint. When a process creates a Unix domain socket and then forks, the child process inherits the file descriptor by default. However, the behavior of socket state and connection handling during fork operations presents unresolved questions about resource management and process isolation.
The socket file's lifecycle is tied to the creating process, with unlink() removing the socket and closing it. Yet the interaction between fork(), exec(), and socket file descriptor inheritance across different Unix implementations requires clarification. Specifically, when multiple processes share a socket file descriptor through inheritance, the permission model and access control mechanisms may not behave as expected.
The core uncertainty lies in whether socket file permissions are re-evaluated for each connection attempt from inherited file descriptors, or if the original permission check at socket creation time persists. This becomes particularly relevant in daemon architectures where the parent process creates a listening socket, forks workers, and expects consistent access control behavior across all child processes.
Which specific permission checks apply when a child process attempts to connect to a Unix domain socket through an inherited file descriptor, and how do these differ between Linux, BSD, and macOS implementations?
None. When a child process inherits a Unix domain socket file descriptor across fork(), no permission check is performed on that inheritance or on subsequent use of the descriptor. Permission checks happen only at the moment a path is resolved — bind() for the listening side and connect() for the client side. An open file descriptor already carries its access rights, so read(), write(), and accept() on an inherited descriptor never re-evaluate the socket file's mode bits or directory permissions.
The confusion in the question comes from conflating two different operations:
connect() to a pathname socket requires write permission on the socket file itself, plus search (execute) permission on every directory in the path. These checks use the connecting process's credentials at the time of the call.fork(), or received via SCM_RIGHTS — the kernel does not re-check filesystem permissions for I/O on it.So in the daemon architecture you describe — parent binds a listening socket, forks workers — the workers' accept() calls perform no filesystem permission check. The only access control that ever applied was the check on each client's connect(), which happened in the client process, not in your daemon.
The per-connect() check is where implementations diverge:
| Platform | Socket file mode enforced on connect()? |
|---|---|
| Linux | Yes — write permission on the socket file plus directory search permission |
| BSD / macOS | Historically not enforced; directory search permissions govern access |
The portable assumption is that socket-file mode bits are not a reliable access control mechanism. Exact BSD and macOS behavior has varied across releases, so treat enforcement as Linux-specific unless you have verified otherwise on your target version. Note also that Linux abstract-namespace sockets (a leading NUL byte in sun_path) have no filesystem node at all, so file permissions never apply to them.
chmod or even unlink() of the path. Unlinking only blocks new pathname connects; existing descriptors keep working — a fact that often surprises daemon-restart logic.SO_PEERCRED on Linux, LOCAL_PEERCRED on FreeBSD/macOS, or getpeereid() on BSD/macOS give you the peer's UID/GID regardless of filesystem semantics.fork() unconditionally but survive exec() only when FD_CLOEXEC is clear. Leaking a listening socket into an exec'd helper is a classic bug; use SOCK_CLOEXEC or fcntl(F_SETFD).On Linux, bind a socket, chmod 000 the path, and attempt a fresh connect() from a different UID — expect EACCES. Then connect first, fork, and confirm the child can still read and write the inherited descriptor after the path is chmod'd or unlinked. Repeating the first test on macOS or a BSD demonstrates the enforcement difference. strace (Linux) or dtruss (macOS) will show that I/O on inherited descriptors issues no path-based permission syscalls at all.
One caveat: the BSD/macOS side of this comparison is the part most likely to shift between OS releases, so if your deployment targets a specific version, verify there rather than trusting the historical behavior.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.