1.5. Managing File Descriptors Safely

Problem

When your program starts up, you want to make sure that only the standard stdin , stdout, and stderr file descriptors are open, thus avoiding denial of service attacks and avoiding having an attacker place untrusted files on special hardcoded file descriptors.

Solution

On Unix, use the function getdtablesize( ) to obtain the size of the process’s file descriptor table. For each file descriptor in the process’s table, close the descriptors that are not stdin, stdout, or stderr, which are always 0, 1, and 2, respectively. Test stdin, stdout, and stderr to ensure that they’re open using fstat( ) for each descriptor. If any one is not open, open /dev/null and associate with the descriptor. If the program is running setuid, stdin, stdout, and stderr should also be closed if they’re not associated with a tty, and reopened using /dev/null.

On Windows, there is no way to determine what file handles are open, but the same issue with open descriptors does not exist on Windows as it does on Unix.

Discussion

Normally, when a process is started, it inherits all open file descriptors from its parent. This can be a problem because the size of the file descriptor table on Unix is typically a fixed size. The parent process could therefore fill the file descriptor table with bogus files to deny your program any file handles for opening its own files. The result is essentially a denial of service for your program.

When a new file is opened, a descriptor is assigned using the first available entry in the process’s file descriptor table. If stdin is not open, for example, the first file opened is assigned a file descriptor of 0, which is normally reserved for stdin. Similarly, if stdout is not open, file descriptor 1 is assigned next, followed by stderr’s file descriptor of 2 if it is not open.

The only file descriptors that should remain open when your program starts are the stdin, stdout, and stderr descriptors. If the standard descriptors are not open, your program should open them using /dev/null and leave them open. Otherwise, calls to functions like printf( ) can have unexpected and potentially disastrous effects. Worse, the standard C library considers the standard descriptors to be special, and some functions expect stderr to be properly opened for writing error messages to. If your program opens a data file for writing and gets stderr’s file descriptor, an error message written to stderr will destroy your data file.

Warning

Particularly in a chroot( ) environment (see Recipe 2.12), the /dev/null device may not be available (it can be made available if the environment is set up properly). If it is not available, the proper thing for your program to do is to refuse to run.

The potential for security vulnerabilities arising from file descriptors being managed improperly is high in non-setuid programs. For setuid (especially setuid root) programs, the potential for problems increases dramatically. The problem is so serious that some variants of Unix (OpenBSD, in particular) will explicitly open stdin, stdout, and stderr from the execve( ) system call for a setuid process if they’re not already open.

The following function, spc_sanitize_files( ) , first closes all open file descriptors that are not one of the standard descriptors. Because there is no easy way to tell whether a descriptor is open, close( ) is called for each one, and any error returned is ignored. Once all of the nonstandard descriptors are closed, stdin, stdout, and stderr are checked to ensure that they are open. If any one of them is not open, an attempt is made to open /dev/null. If /dev/null cannot be opened, the program is terminated immediately.

#include <sys/types.h>
#include <limits.h>
#include <sys/stat.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <paths.h>
   
#ifndef OPEN_MAX
#define OPEN_MAX 256
#endif
   
static int open_devnull(int fd) {
  FILE *f = 0;
   
  if (!fd) f = freopen(_PATH_DEVNULL, "rb", stdin);
  else if (fd =  = 1) f = freopen(_PATH_DEVNULL, "wb", stdout);
  else if (fd =  = 2) f = freopen(_PATH_DEVNULL, "wb", stderr);
  return (f && fileno(f) =  = fd);
}
   
void spc_sanitize_files(void) {
  int         fd, fds;
  struct stat st;
   
  /* Make sure all open descriptors other than the standard ones are closed */
  if ((fds = getdtablesize(  )) =  = -1) fds = OPEN_MAX;
  for (fd = 3;  fd < fds;  fd++) close(fd);
   
  /* Verify that the standard descriptors are open.  If they're not, attempt to
   * open them using /dev/null.  If any are unsuccessful, abort.
   */
  for (fd = 0;  fd < 3;  fd++)
    if (fstat(fd, &st) =  = -1 && (errno != EBADF || !open_devnull(fd))) abort(  );
}

Get Secure Programming Cookbook for C and C++ now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.