Catching FD leaks
When spawning a subprocess using the fork/exec family syscalls,
file descriptors can be leaked to the subprocesses if the code does not mark them as O_CLOEXEC or close them after fork.
FD leaks can cause bugs or even security vulnerabilities.
In this tutorial, we will write a small launcher that accidentally passes its log file
to a worker, find the descriptor with tracexec and fix the leak.
You will need Linux, tracexec and a C compiler available as cc.
The worker is the external printf program, which should be available in your PATH.
Here is a recording of the example, the investigation and the fix:
The Example Program
Our launcher opens launcher.log, writes a message and forks a child to run printf.
The parent closes the log and waits for the worker to finish.
That sounds reasonable, but there is a missing piece.
Create a directory for the example:
mkdir fd-leaks
cd fd-leaks
Save the following as launcher.c. The complete source is also available in
book/tutorials/fd-leaks in the tracexec repository.
#define _POSIX_C_SOURCE 200809L
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <sys/wait.h>
#include <unistd.h>
int main(void)
{
int log_fd = open("launcher.log", O_WRONLY | O_CREAT | O_APPEND, 0600);
if (log_fd == -1) {
perror("open");
return 1;
}
if (dprintf(log_fd, "Starting worker\n") < 0) {
perror("write log");
close(log_fd);
return 1;
}
pid_t child = fork();
if (child == -1) {
perror("fork");
close(log_fd);
return 1;
}
if (child == 0) {
execlp("printf", "printf", "%s\n", "Worker finished", (char *)NULL);
perror("exec printf");
_exit(127);
}
/* The parent no longer needs the log. */
int close_failed = close(log_fd) == -1;
if (close_failed) {
perror("close log");
}
int status;
while (waitpid(child, &status, 0) == -1) {
if (errno != EINTR) {
perror("waitpid");
return 1;
}
}
if (close_failed || !WIFEXITED(status)) {
return 1;
}
return WEXITSTATUS(status);
}
Compile and run it:
cc -std=c11 -Wall -Wextra -o launcher launcher.c
./launcher
Worker finished
It also appends Starting worker to launcher.log.
There is no error message, and the program exits successfully.
The problem is what the worker inherited along the way.
Finding the Leaked Descriptor
Trace the launcher:
tracexec tui -- ./launcher
You should see an exec event for launcher, followed by one for printf.
Switch to the Events pane with Ctrl+S, select the successful
printf event and press V to open its details.
Depending on your PATH, there may be failed attempts to find printf before the successful one.
Press → twice to switch to FdInfo, then End to scroll to the bottom.
Alongside stdin, stdout and stderr, you will find another descriptor pointing to
launcher.log. It is descriptor 3 in the recording, but the exact number can differ.
The entry shows the full path and flags such as O_WRONLY and O_APPEND.
This is the launcher’s log, yet it appears in the worker’s exec event.
printf has no reason to use this file descriptor thus it is a leak.
Fixing the Leak
Press Q to close the details, then Q again to leave tracexec.
Add O_CLOEXEC to the flags passed to open:
int log_fd = open("launcher.log", O_WRONLY | O_CREAT | O_APPEND | O_CLOEXEC, 0600);
This marks the descriptor to be closed automatically when the child successfully executes the worker. It can still be used to write the log before exec.
The recording makes this edit with:
sed -i 's/O_APPEND,/O_APPEND | O_CLOEXEC,/' launcher.c
Recompile and trace the program again:
cc -std=c11 -Wall -Wextra -o launcher launcher.c
tracexec tui -- ./launcher
Open the successful printf event and check FdInfo again.
The launcher.log entry is gone and thus the leak is solved.