Handling closed standard file descriptors
=========================================

Usually, when a program gets invoked, its file descriptors 0 (for standard input), 1 (for standard output), and 2 (for standard error) are open.  But there are situations when some of these file descriptors are closed.  These situations can arise when

  * The invoking process invokes close() on the file descriptor before exec, or

  * The invoking process invokes posix_spawn_file_actions_addclose() for the file descriptor before posix_spawn or posix_spawnp, or

  * The invoking process is a Bourne shell, and the shell script uses the POSIX syntax for closing the file descriptor: <&- for closing standard input, >&- for closing standard output, or 2>&- for closing standard error.

When a closed file descriptor is accessed through a system call, such as fcntl(), fstat(), read(), or write(), the system calls fails with error EBADF ("Bad file descriptor").

When a new file descriptor is allocated, the operating system chooses the smallest non-negative integer that does not yet correspond to an open file descriptor.  So, when a given fd (0, 1, or 2) is closed, opening a new file descriptor may assign the new file descriptor to this fd.  This can have unintended effects, because now standard input/output/error of your process is referring to a file that was not meant to be used in that role.

This situation is a security risk because the behaviour of the program in this situation was surely never tested, therefore anything can happen then -- from overwriting precious files of the user to endless loops.

To deal with this situation, you first need to determine whether your program is affected by the problem.

  * Does your program invoke functions that allocate new file descriptors? These are the system calls

       * open(), openat(), creat()

       * dup()

       * fopen(), freopen()

       * pipe(), pipe2(), popen()

       * opendir()

       * tmpfile(), mkstemp(), mkstemps(), mkostemp(), mkostemps()

     Note that you also have to consider the libraries that your program uses.

  * If your program may open two or more file descriptors or FILE streams for reading at the same time, and some of them may reference standard input, your program is affected.

  * If your program may open two or more file descriptors or FILE streams for writing at the same time, and some of them may reference standard output or standard error, your program is affected.

  * If your program does not open new file descriptors or FILE streams, it is not affected.

  * If your program opens only one new file descriptor or FILE stream at a time, it is not affected.  This is often the case for programs that are structured in simple phases: first a phase where input is read from a file into memory, then a phase of processing in memory, finally a phase where the result is written to a file.

  * If your program opens only two new file descriptors or FILE streams at a time, out of which one is for reading and the one is for writing, it is not affected.  This is because if the first file descriptor is allocated and the second file descriptor is picked as 0, 1, or 2, and both happen to be the same, writing to the one opened in O_RDONLY mode will produce an error EBADF, as desired.

If your program is affected, what is the mitigation?

Some operating systems install open file descriptors in place of the closed ones, either in the exec system call or during program startup.  When such a file descriptor is accessed through a system call, it behaves like an open file descriptor opened for the ``wrong'' direction: the system calls fcntl() and fstat() succeed, whereas read() from fd 0 and write() to fd 1 or 2 fail with error EBADF ("Bad file descriptor").  The important point here is that when your program allocates a new file descriptor, it will have a value greater than 2.

This mitigation is enabled on HP-UX, for all programs, and on glibc, FreeBSD, NetBSD, OpenBSD, but only for setuid or setgid programs.  Since it is operating system dependent, it is not a complete mitigation.

For a complete mitigation, Gnulib provides two alternative sets of modules:

  * The xstdopen module.

  * The *-safer modules: fcntl-safer, openat-safer, unistd-safer, fopen-safer, freopen-safer, pipe2-safer, popen-safer, dirent-safer, tmpfile-safer, stdlib-safer.

The approach with the xstdopen module is simpler, but it adds three system calls to program startup.  Whereas the approach with the *-safer modules is more complex, but adds no overhead (no additional system calls) in the normal case.

To use the approach with the xstdopen module:

 1. Import the module xstdopen from Gnulib.

 2. In the compilation unit that contains the main function, include "xstdopen.h".

 3. In the main function, near the beginning, namely right after the i18n related initializations (setlocale, bindtextdomain, textdomain invocations, if any) and the closeout initialization (if any), insert the invocation:

          /* Ensure that stdin, stdout, stderr are open.  */
          xstdopen ();

To use the approach with the *-safer modules:

 1. Import the relevant modules from Gnulib.

 2. In the compilation units that contain these function calls, include the replacement header file.

Do so according to this table:

Function   Module   Header file
open()
fcntl-safer
"fcntl--.h"
openat()
openat-safer
"fcntl--.h"
creat()
fcntl-safer
"fcntl--.h"
dup()
unistd-safer
"unistd--.h"
fopen()
fopen-safer
"stdio--.h"
freopen()
freopen-safer
"stdio--.h"
pipe()
unistd-safer
"unistd--.h"
pipe2()
pipe2-safer
"unistd--.h"
popen()
popen-safer
"stdio--.h"
opendir()
dirent-safer
"dirent--.h"
tmpfile()
tmpfile-safer
"stdio--.h"
mkstemp()
stdlib-safer
"stdlib--.h"
mkstemps()
stdlib-safer
"stdlib--.h"
mkostemp()
stdlib-safer
"stdlib--.h"
mkostemps()
stdlib-safer
"stdlib--.h"