mirror of
git://git.sv.gnu.org/findutils.git
synced 2025-09-10 02:58:42 +02:00
Run 'make update-copyright'. * lib/regexprops.c (copying): Update the year number manually. * tests/sample-test: Adjust to use the single most recent year. * All other files: Update copyright years via the above make run.
397 lines
9.1 KiB
C
397 lines
9.1 KiB
C
/* fdleak.c -- detect file descriptor leaks
|
|
Copyright (C) 2010-2025 Free Software Foundation, Inc.
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
*/
|
|
/* config.h must be included first. */
|
|
#include <config.h>
|
|
|
|
/* system headers. */
|
|
#include <assert.h>
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <limits.h>
|
|
#include <poll.h>
|
|
#include <stdarg.h>
|
|
#include <stdbool.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#if HAVE_GETRLIMIT
|
|
# include <sys/resource.h>
|
|
#endif
|
|
#include <unistd.h>
|
|
|
|
/* gnulib headers. */
|
|
#include "cloexec.h"
|
|
#include "dirent--.h"
|
|
#include "fcntl--.h"
|
|
|
|
/* find headers. */
|
|
#include "system.h"
|
|
#include "extendbuf.h"
|
|
#include "fdleak.h"
|
|
#include "safe-atoi.h"
|
|
|
|
|
|
/* In order to detect FD leaks, we take a snapshot of the open
|
|
* file descriptors which are not FD_CLOEXEC when the program starts.
|
|
* When the program exits, we discover if there are any new
|
|
* file descriptors which aren't FD_CLOEXEC.
|
|
*/
|
|
static int *non_cloexec_fds;
|
|
static size_t num_cloexec_fds;
|
|
|
|
|
|
/* Determine the value of the largest open fd, on systems that
|
|
* offer /proc/self/fd. */
|
|
static int
|
|
get_proc_max_fd (void)
|
|
{
|
|
const char *path = "/proc/self/fd";
|
|
int maxfd = -1;
|
|
/* We don't use readdir_r, because we cannot trust pathconf
|
|
* to tell us the maximum possible length of a path in
|
|
* a given directory (the manpage for readdir_r claims this
|
|
* is the approved method, but the manpage for pathconf indicates
|
|
* that _PC_NAME_MAX is not an upper limit). */
|
|
DIR *dir = opendir (path);
|
|
if (dir)
|
|
{
|
|
int good = 0;
|
|
struct dirent *dent;
|
|
|
|
while (1)
|
|
{
|
|
errno = 0;
|
|
dent = readdir (dir);
|
|
if (NULL == dent)
|
|
{
|
|
if (errno)
|
|
{
|
|
error (0, errno, "%s", quotearg_n_style (0, locale_quoting_style, path));
|
|
good = 0;
|
|
}
|
|
break;
|
|
}
|
|
|
|
if (dent->d_name[0] != '.'
|
|
|| (dent->d_name[1] != 0
|
|
&& (dent->d_name[1] != '.' || dent->d_name[2] != 0)))
|
|
{
|
|
const int fd = safe_atoi (dent->d_name, literal_quoting_style);
|
|
if (fd > maxfd)
|
|
maxfd = fd;
|
|
good = 1;
|
|
}
|
|
}
|
|
closedir (dir);
|
|
if (good)
|
|
return maxfd;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
|
|
|
|
/* Estimate the value of the largest possible file descriptor */
|
|
static int
|
|
get_max_fd (void)
|
|
{
|
|
long open_max;
|
|
|
|
open_max = get_proc_max_fd ();
|
|
if (open_max >= 0)
|
|
return open_max;
|
|
|
|
open_max = sysconf (_SC_OPEN_MAX);
|
|
if (open_max == -1)
|
|
open_max = _POSIX_OPEN_MAX; /* underestimate */
|
|
|
|
/* We assume if RLIMIT_NOFILE is defined, all the related macros are, too. */
|
|
#if defined HAVE_GETRLIMIT && defined RLIMIT_NOFILE
|
|
{
|
|
struct rlimit fd_limit;
|
|
if (0 == getrlimit (RLIMIT_NOFILE, &fd_limit))
|
|
{
|
|
if (fd_limit.rlim_cur == RLIM_INFINITY)
|
|
return open_max;
|
|
else
|
|
return (int) fd_limit.rlim_cur;
|
|
}
|
|
}
|
|
#endif
|
|
/* cannot determine the limit's value */
|
|
return open_max;
|
|
}
|
|
|
|
|
|
static int
|
|
visit_open_fds (int fd_min, int fd_max,
|
|
int (*callback)(int, void*), void *cb_context)
|
|
{
|
|
enum { MAX_POLL = 64 };
|
|
struct pollfd pf[MAX_POLL];
|
|
int rv = 0;
|
|
|
|
while (fd_min < fd_max)
|
|
{
|
|
int i;
|
|
int limit = fd_max - fd_min;
|
|
if (limit > MAX_POLL)
|
|
limit = MAX_POLL;
|
|
|
|
for (i=0; i<limit; i++)
|
|
{
|
|
pf[i].events = POLLIN|POLLOUT;
|
|
pf[i].revents = 0;
|
|
pf[i].fd = fd_min + i;
|
|
}
|
|
rv = poll (pf, limit, 0);
|
|
if (-1 == rv)
|
|
{
|
|
return -1;
|
|
}
|
|
else
|
|
{
|
|
int j;
|
|
for (j=0; j<limit; j++)
|
|
{
|
|
if (pf[j].revents != POLLNVAL)
|
|
{
|
|
if (0 != (rv = callback (pf[j].fd, cb_context)))
|
|
return rv;
|
|
}
|
|
}
|
|
}
|
|
fd_min += limit;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
fd_is_cloexec (int fd)
|
|
{
|
|
const int flags = fcntl (fd, F_GETFD);
|
|
return flags & FD_CLOEXEC;
|
|
}
|
|
|
|
|
|
/* Faking closures in C is a bit of a pain. */
|
|
struct remember_fd_context
|
|
{
|
|
int *buf;
|
|
size_t used;
|
|
size_t allocated;
|
|
};
|
|
|
|
|
|
/* Record FD is it's not FD_CLOEXEC. */
|
|
static int
|
|
remember_fd_if_non_cloexec (int fd, void *context)
|
|
{
|
|
if (fd_is_cloexec (fd))
|
|
{
|
|
return 0;
|
|
}
|
|
else
|
|
{
|
|
struct remember_fd_context * const p = context;
|
|
void *newbuf = extendbuf (p->buf,
|
|
sizeof (p->buf[0])*(p->used+1),
|
|
&(p->allocated));
|
|
if (newbuf)
|
|
{
|
|
p->buf = newbuf;
|
|
p->buf[p->used] = fd;
|
|
++p->used;
|
|
return 0;
|
|
}
|
|
else
|
|
{
|
|
return -1;
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
remember_non_cloexec_fds (void)
|
|
{
|
|
int max_fd = get_max_fd ();
|
|
struct remember_fd_context cb_data;
|
|
cb_data.buf = NULL;
|
|
cb_data.used = cb_data.allocated = 0;
|
|
|
|
if (max_fd < INT_MAX)
|
|
++max_fd;
|
|
visit_open_fds (0, max_fd, remember_fd_if_non_cloexec, &cb_data);
|
|
|
|
non_cloexec_fds = cb_data.buf;
|
|
num_cloexec_fds = cb_data.used;
|
|
}
|
|
|
|
|
|
struct fd_leak_context
|
|
{
|
|
const int *prev_buf;
|
|
size_t used;
|
|
size_t lookup_pos;
|
|
int leaked_fd;
|
|
};
|
|
|
|
/* FD is open and not close-on-exec.
|
|
* If it's not in the list of non-cloexec file descriptors we saw before, it's a leak.
|
|
*/
|
|
static int
|
|
find_first_leak_callback (int fd, void *context)
|
|
{
|
|
if (!fd_is_cloexec (fd))
|
|
{
|
|
struct fd_leak_context *p = context;
|
|
while (p->lookup_pos < p->used)
|
|
{
|
|
if (p->prev_buf[p->lookup_pos] < fd)
|
|
{
|
|
++p->lookup_pos;
|
|
}
|
|
else if (p->prev_buf[p->lookup_pos] == fd)
|
|
{
|
|
/* FD was open and still is, it's not a leak. */
|
|
return 0;
|
|
}
|
|
else
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
/* We come here if p->prev_buf[p->lookup_pos] > fd, or
|
|
if we ran out of items in the lookup table.
|
|
Either way, this is a leak. */
|
|
p->leaked_fd = fd;
|
|
return -1; /* No more callbacks needed. */
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
|
|
static int
|
|
find_first_leaked_fd (const int* prev_non_cloexec_fds, size_t n)
|
|
{
|
|
struct fd_leak_context context;
|
|
int max_fd = get_max_fd ();
|
|
|
|
if (max_fd < INT_MAX)
|
|
++max_fd;
|
|
context.prev_buf = prev_non_cloexec_fds;
|
|
context.used = n;
|
|
context.lookup_pos = 0;
|
|
context.leaked_fd = -1;
|
|
visit_open_fds (0, max_fd, find_first_leak_callback, &context);
|
|
return context.leaked_fd;
|
|
}
|
|
|
|
/* Determine if O_CLOEXEC actually works (Savannah bug #29435:
|
|
fd_is_cloexec () does not work on Fedora buildhosts).
|
|
*/
|
|
static bool
|
|
o_cloexec_works (void)
|
|
{
|
|
bool result = false;
|
|
int fd = open ("/", O_RDONLY|O_CLOEXEC);
|
|
if (fd >= 0)
|
|
{
|
|
result = fd_is_cloexec (fd);
|
|
close (fd);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
|
|
int
|
|
open_cloexec (const char *path, int flags, ...)
|
|
{
|
|
int fd;
|
|
mode_t mode = 0;
|
|
static bool cloexec_works = false;
|
|
static bool cloexec_status_known = false;
|
|
|
|
if (flags & O_CREAT)
|
|
{
|
|
/* this code is copied from gnulib's open-safer.c. */
|
|
va_list ap;
|
|
va_start (ap, flags);
|
|
|
|
/* We have to use PROMOTED_MODE_T instead of mode_t, otherwise GCC 4
|
|
creates crashing code when 'mode_t' is smaller than 'int'. */
|
|
mode = va_arg (ap, PROMOTED_MODE_T);
|
|
|
|
va_end (ap);
|
|
}
|
|
|
|
/* Kernels usually ignore open flags they don't recognise, so it
|
|
* is possible this program was built against a library which
|
|
* defines O_CLOEXEC, but is running on a kernel that (silently)
|
|
* does not recognise it. We figure this out by just trying it,
|
|
* once.
|
|
*/
|
|
if (!cloexec_status_known)
|
|
{
|
|
cloexec_works = o_cloexec_works ();
|
|
cloexec_status_known = true;
|
|
}
|
|
fd = open (path, flags|O_CLOEXEC, mode);
|
|
if ((fd >= 0) && !(O_CLOEXEC && cloexec_works))
|
|
{
|
|
set_cloexec_flag (fd, true);
|
|
}
|
|
return fd;
|
|
}
|
|
|
|
void
|
|
forget_non_cloexec_fds (void)
|
|
{
|
|
free (non_cloexec_fds);
|
|
non_cloexec_fds = NULL;
|
|
num_cloexec_fds = 0;
|
|
}
|
|
|
|
/* Return nonzero if file descriptor leak-checking is enabled.
|
|
*/
|
|
bool
|
|
fd_leak_check_is_enabled (void)
|
|
{
|
|
if (getenv ("GNU_FINDUTILS_FD_LEAK_CHECK"))
|
|
return true;
|
|
else
|
|
return false;
|
|
|
|
}
|
|
|
|
void
|
|
complain_about_leaky_fds (void)
|
|
{
|
|
int no_leaks = 1;
|
|
const int leaking_fd = find_first_leaked_fd (non_cloexec_fds, num_cloexec_fds);
|
|
|
|
if (leaking_fd >= 0)
|
|
{
|
|
no_leaks = 0;
|
|
error (0, 0,
|
|
_("File descriptor %d will leak; please report this as a bug, "
|
|
"remembering to include a detailed description of the simplest "
|
|
"way to reproduce this problem."),
|
|
leaking_fd);
|
|
}
|
|
assert (no_leaks);
|
|
}
|