Managing File Descriptor Leaks in Unix Fork-Exec Workflows
Unix fork-exec preserves file descriptors by design, which can lead to security leaks. Learn how to use FD_CLOEXEC and strace to manage descriptor inheritance and prevent resource leakage.
19 Jul 2025, 12:28 UTC

You spawn a helper process using fork and exec, only to find that the child process unexpectedly keeps a database socket or a sensitive log file open. This happens because the child inherits every open file descriptor (FD) from the parent, not just the standard streams. The critical takeaway is to treat descriptor inheritance as an explicit contract: assume everything is inherited unless you mark it close-on-exec, and manually close what you do not need before the exec call.
The Mechanics of the Descriptor Leak
When fork() is called, the kernel duplicates the calling process. The child receives a copy of the parent's file descriptor table. These descriptors point to the same open file descriptions in the kernel, meaning they share the same file offset and status flags. If the parent has a file open at offset 100, the child starts at offset 100.
The execve() family of functions then replaces the process image with a new program. By design, exec preserves the descriptor table. This is what enables Unix pipelines; a shell can dup2 a pipe end onto stdout, fork, and exec a command that writes to that pipe without the command needing to know the pipe exists. However, for a long-running daemon, this means every child inherits every open socket, file, and timer, potentially exposing sensitive handles to untrusted code or preventing filesystem unmounts.
Portable Control via FD_CLOEXEC
To prevent this leakage, POSIX provides the FD_CLOEXEC flag. When this flag is set on a descriptor, the kernel automatically closes that descriptor when any exec function is called. While this sounds straightforward, the default behavior varies across Unix flavors. Linux historically creates new descriptors with FD_CLOEXEC off, whereas some BSD-derived systems or specific APIs may differ. Relying on platform defaults is a portability risk.
The most reliable pattern is to set the flag immediately after opening the resource using fcntl. Here is a concrete implementation in C:
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
int main(void) {
// Open a file that should NOT be inherited by children
int fd = open("/tmp/secret_log", O_RDWR | O_CREAT, 0644);
if (fd < 0) {
perror("open");
return 1;
}
// Explicitly mark the descriptor to close on exec
int flags = fcntl(fd, F_GETFD);
if (flags != -1) {
fcntl(fd, F_SETFD, flags | FD_CLOEXEC);
}
pid_t pid = fork();
if (pid == 0) {
// Child: exec replaces the image.
// Because FD_CLOEXEC is set, /tmp/secret_log is closed automatically.
execl("/usr/bin/whoami", "whoami", NULL);
_exit(127);
}
// Parent continues and closes its own handle
close(fd);
return 0;
}To run this, use a standard C compiler (e.g., gcc) on a POSIX-compliant system. No elevated privileges are required for these operations. The expected result is that the whoami process will not have the secret_log file descriptor in its table.
Verifying Inheritance with strace
You can diagnose descriptor leaks without modifying your source code using strace. By tracing the fork and exec sequence, you can see exactly which descriptors remain open during the transition.
Run the following command as a user with permission to trace the target process:
strace -f -e trace=fork,execve,close,dup2 [your_command]The -f flag ensures that child processes are also traced. Look for the execve call; any file descriptor that was opened before the execve and not explicitly closed via close() or marked with FD_CLOEXEC will be leaked into the new process image.
Trade-offs and Limitations
Setting FD_CLOEXEC on every descriptor is the safest approach, but it requires disciplined bookkeeping. An alternative is to iterate through all open descriptors in the child process after fork but before exec and close everything except 0, 1, and 2. However, this is computationally expensive and requires scanning /proc/self/fd on Linux, which is not portable to all Unix systems.
Additionally, remember that fork also inherits the parent's signal mask. If the parent has blocked certain signals, the child will also have them blocked after exec, which can lead to programs that appear unresponsive to termination signals. Always reset your signal mask in the child if you require a clean state.
Actionable Closing: Audit your process-spawning logic. Set FD_CLOEXEC immediately upon opening any resource that is not intended for a child. Use strace to verify that your execve calls are not carrying unnecessary baggage into the new process image.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.