< Previous PageNext Page > Hide TOC

Avoiding Race Conditions and Insecure File Operations

This article describes the various sorts of race conditions and insecure file operations and discusses how to avoid them. Code samples illustrate safe practices.

Contents:

Race Conditions Explained
Interprocess Communication
Insecure File Operations
Secure File Operations
Time Of Check–Time Of Use


Race Conditions Explained

Suppose you wrote a program designed to automatically count the number of people entering a sports stadium for a game. The turnstiles are wired and send a signal to the computer each time someone walks through. You have a separate process running to monitor the signal from each turnstile. Each time a process receives a signal, it reads the global variable Gate, increments it by one, and writes it back. Thus, multiple processes are keeping a single running total. Now suppose two people enter different gates at exactly the same time. The sequence of events might then be as follows:

  1. Process A receives a signal from gate A.

  2. Process B receives a signal from gate B.

  3. Process A reads Gate == 1000.

  4. Process B reads Gate == 1000.

  5. Process A increments Gate by 1 so that Gate == 1001.

  6. Process B increments Gate by 1 so that Gate == 1001.

  7. Process A writes Gate = 1001.

  8. Process B writes Gate = 1001.

Because process B read Gate before process A had time to increment it and write it back, both process A and process B have read the same value for Gate. After process A increments Gate and writes it back, process B overwrites the value of Gate with the same value written by process A. Because of the race condition, one of the two people entering the stadium was not counted. Since there might be long lines at each turnstile, this condition might occur many times before a big game, and a dishonest ticket clerk who knew about this undercount could pocket some of the receipts with no fear of being caught.

From a software security point of view, there are a couple of ways to exploit race conditions. If a program is writing temporary files, or temporarily relaxing permissions on files or folders in order to perform a privileged operation, an attacker might be able to create a race condition by careful timing of his attack. If the program checks the status of a file before writing to it, for example, the attacker might be able to take advantage of the time gap between when the program checks the file and when it writes to it to mount an attack. This is referred to as a time of check–time of use problem.

Other race conditions that can be exploited, like the example above, involve the use of shared data or other interprocess communication methods. If an attacker can interfere with the data after it is written and before it is read, he can disrupt the operation of the program, alter data, or do other mischief. The use of non-thread-safe calls in multithreaded programs can result in data corruption. If an attacker can manipulate the program to cause two such threads to interfere with each other, it may be possible to mount a denial-of-service attack. In some cases, by using such a race condition to overwrite a buffer in the heap with data from a routine that uses more data than the routine that allocated the buffer, an attacker can create a buffer overflow. As discussed in “Avoiding Buffer Overflows,” buffer overflows can be exploited to cause execution of malicious code. Darwin-level code (that is, scripts and code written with direct calls to BSD) that includes signal handlers is especially vulnerable to this sort of attack.

Interprocess Communication

Any time the sequence in which two operations are completed affects the result, there is the potential for a race condition. For example, if two processes (in a single program or different programs) share the same global variable, then there is the potential for one process to interfere with the other or for an attacker to alter the variable after one process sets it but before the other reads it. See “Race Conditions Explained” at the beginning of this article for an example of a race condition of this type. The solution to race conditions of this type is to use some locking mechanism to prevent one process from changing a variable until another is finished with it. There are problems and hazards associated with such mechanisms, however, and they must be implemented carefully. For a full discussion, see Wheeler, Secure Programming for Linux and Unix HOWTO, at http://www.dwheeler.com/secure-programs/.

Signal handlers are another common source of race conditions. Signals from the operating system to a process or between two processes are used for such purposes as terminating a process or causing it to reinitialize. If you include signal handlers in your program, they should not make any system calls and should terminate as quickly as possible. Although there are certain system calls that are safe from within signal handlers, writing a safe signal handler that does so is tricky. The best thing to do is to set a flag that your program checks periodically, and do no other work within the signal handler. This is because the signal handler can be interrupted by a new signal before it finishes processing the first signal, leaving the system in an unpredictable state or, worse, providing a vulnerability for an attacker to exploit. For example, if the signal handler writes user-supplied data to a system log, an attacker can use a signal handler race condition to put the attacker's own code into the heap.

In a vulnerability reported in 1997 for a number of implementations of the FTP protocol, a user could cause a race condition by closing an FTP connection. Closing the connection resulted in the near-simultaneous transmission of two signals to the FTP server: one to abort the current operation, and one to log out the user. The race condition occurred when the logout signal arrived just before the abort signal. When a user logged onto an FTP server as an anonymous user, the server would temporarily downgrade its privileges from root to nobody so that the logged-in user had no privileges to write files. In order to log out the user, however, the server reassumed root privileges. If the abort signal arrived at just the right time, it would abort the logout procedure after the server had assumed root privileges but before it had logged out the user. The user would then be logged in with root privileges, and could proceed to write files at will. An attacker could exploit this vulnerability with a graphical FTP client simply by repeatedly clicking the “Cancel” button. [CVE-1999-0035]

For a brief introduction to signal handlers, see the Little Unix Programmers Group site at http://users.actcom.co.il/~choo/lupg/tutorials/signals/signals-programming.html. For a discourse on how signal handler race conditions can be exploited, see the article by Michal Zalewski at http://www.bindview.com/Services/razor/Papers/2001/signals.cfm.

Insecure File Operations

Insecure file operations are a major source of security vulnerabilities. In some cases, opening or writing to a file in an insecure fashion can give attackers the opportunity to create a race condition (see “Time Of Check–Time Of Use”). Often, however, insecure file operations give an attacker the chance to read confidential information, an opportunity to gain control of an application or even of the system, or an opening for a denial of service attack. This section discusses insecure file operations. The following section, “Secure File Operations,” describes some techniques you can use to make sure your file operations are secure.

Secure File Operations

There are several principles you can follow to help ensure that you do not have file-based security vulnerabilities in your program:

The following sections give some hints on how to follow these principles when you are using generic C code, Carbon, and Cocoa.

Generic C

For generic C programming, if you are opening a temporary file in a public directory, you can use the open function with the O_CREAT and O_EXCL flags set to create the file and obtain a file descriptor. The O_EXCL flag causes this function to return an error if the file already exists. Be sure to check for errors before proceeding. As a shortcut, you can use the mkstemp function to open the temporary file. The mkstemp function guarantees a unique filename and returns a file descriptor, thus allowing you skip the step of checking the open function result for an error, which might require you to change the filename and call open again.

After you've opened the file and obtained a file descriptor, you can safely use functions that take file descriptors, such as the standard C functions write and read, for as long as you keep the file open. See the manual pages for open(2), mkstemp(3), write(2), and read(2) for more on these functions, and see Wheeler, Secure Programming for Linux and Unix HOWTO for advantages and shortcomings to using these functions.

If you need to open a preexisting file to modify it or read from it, you need to check the file's ownership, type, and permissions, and the number of links to the file before using it.

To safely opening a file for reading, for example, you can use the following procedure:

  1. Call the lstat function to get information about the file. (Do not use the stat function, as that function follows symbolic links.) Save the stat structure returned by the lstat function.

  2. Check the file's status information to make sure the file is not a symbolic link.

  3. Check the user ID (UID) and group ID (GID) of the file to make sure they are correct.

  4. Check the filetype to make sure it's correct.

  5. Check the read, write, and execute permissions for the file to make sure they are what you expect.

  6. Check that there is only one hard link to the file.

  7. Call the open function and save the file descriptor.

  8. Using the file descriptor, call the fstat function to obtain the stat structure for the file you opened.

  9. Compare the device and inode numbers in the stat structure obtained before you opened the file with those in the stat structure obtained after you opened the file to verify that they are the same file.

  10. Check all the information in the stat structure returned by the fstat function to make sure it is what you expect.

Although this might seem like a lot of extra work, it eliminates the race condition that can occur between calling the stat and open functions. Note that you can avoid all the status checking by using a secure directory instead of a public one to hold your program's files.

Table 1 shows some functions to avoid—and the safer equivalent functions to use—in order to avoid race conditions when you are creating files in a public directory.

Table 1  C file functions to avoid and to use

Functions to avoid

Functions to use instead

fopen returns a file pointer; automatically creates the file if it does not exist but returns no error if the file does exist

open returns a file descriptor; creates a file and returns an error if the file already exists when the O_CREAT and O_EXCL options are used

chmod takes a file path

fchmod takes a file descriptor

chown takes a file path and follows symbolic links

fchown takes a file descriptor and does not follow symbolic links

stat takes a file path and follows symbolic links

lstat takes a file path but does not follow symbolic links;

fstat takes a file descriptor and returns information about an open file

mktemp creates a temporary file with a unique name and returns a file path; you need to open the file in another call

mkstemp creates a temporary file with a unique name, opens it for reading and writing, and returns a file descriptor

Carbon

If you are using the Carbon File Manager to create and open files, you should be aware of how the File Manager accesses files.

To find the default location to store temporary files, you can call the FSFindFolder function and specify a directory type of kTemporaryFolderType. This function checks to see whether the UID calling the function owns the directory and, if not, returns the user home directory in ~/library. Therefore, this function returns a relatively safe place to store temporary files. This location is not as secure as a directory that you created and that is accessible only by your program. The FSFindFolder function is documented in Folder Manager Reference.

If you've obtained the file reference of a directory (from the FSFindFolder function, for example), you can use the FSRefMakePath function to obtain the directory's path name. However, be sure to check the function result, because if the FSFindFolder function fails, it returns a null string. If you don't check the function result, you might end up trying to create a temporary file with a pathname formed by appending a filename to a null string.

Cocoa

There are no Cocoa methods that create a file and return a file descriptor. However, you can call the standard C open function from an Objective-C program to obtain a file descriptor (see “Generic C”). Or you can call the mkstemp function to create a temporary file and obtain a file descriptor. Then you can use the NSFileHandle method InitWithFileDescriptor: to initialize a file handle, and other NSFileHandle methods to safely write to or read from the file. Documentation for the NSFileHandle class is in Foundation Framework Reference.

To obtain the path to the default location to store temporary files (stored in the $TMPDIR environmental variable), you can use the NSTemporaryDirectory function, which calls theFSFindFolder and FSRefMakePath functions for you (see “Carbon”). Note that NSTemporaryDirectory can return /tmp under certain circumstances such as if you link on a pre-Mac OS X v10.3 development target. Therefore, if you're using NSTemporaryDirectory, you either have to be sure that using /tmp is suitable for your operation or, if not, you should consider that an error case and create a more secure temporary directory if that happens.

The changeFileAttributes:atPath: method in the NSFileManager class is similar to chmod or chown, in that it takes a file path rather than a file descriptor. You shouldn't use this method if you're working in a public directory or a user's home directory. Instead, call the fchown or fchmod function (see Table 1). You can call the NSFileHandle class's fileDescriptor method to get the file descriptor of a file in use by NSFileHandle.

The NSString and NSData classes have writeToFile:atomically methods designed to minimize the risk of data loss when writing to a file. These methods write first to a temporary file, and then, when they're sure the write is successful, they replace the written-to file with the temporary file. This is not always an appropriate thing to do when working in a public directory or a user's home directory, because there are a number of path-based file operations involved. Instead, initialize an NSFileHandle object with an existing file descriptor and use NSFileHandle methods to write to the file, as mentioned above. The following code, for example, uses the mkstemp function to create a temporary file and obtain a file descriptor, which it then uses to initialize NSFileHandle:

fd = mkstemp(tmpfile); // check return for -1, which indicates an error
NSFileHandle *myhandle = [[NSFileHandle alloc] initWithFileDescriptor:fd];

Shell Scripts

Scripts must follow the same general rules as other programs to avoid race conditions. There are a few tips you should know to help make your scripts more secure.

First, when writing a script, set the temporary directory ($TMPDIR) environmental variable to a safe directory. Even if your script doesn't directly create any temporary files, one or more of the routines you call might create one, which can be a security vulnerability if it's created in an insecure directory. See the manual pages for setenv(1) and setenv(3) for information on changing the temporary directory environmental variable. For the same reason, set your process' file code creation mask (umask) to restrict access to any files that might be created by routines run by your script (see “Secure File Operations” for more information on the umask).

It's also a good idea to use the ktrace function on a shell script so you can watch every command that gets executed to make sure that during the life of your script no temporary file is created in an insecure location. See the manual page for ktrace(2) for more information.

Do not redirect output using the operators > or >> to a publicly writable location. These operators do not check to see whether the file already exists, and they follow symbolic links.

Do not use the test command (or its left bracket ([) equivalent) to check for the existence of a file or other status information for the file before writing to it. Doing so always results in a race condition; that is, it is possible for an attacker to create, write to, alter, or replace the file before you start writing. Instead, use the mkdtemp command to create a subdirectory to which only you have access. It's important to check the result to make sure the command succeeded. if you do all your file operations in this directory, you can be fairly confident that no one with less than root access can interfere with your script. For more information, see the manual pages for test(1) and mkdtemp(3).

Time Of Check–Time Of Use

A race condition that can be caused by insecure file operations is the time of check–time of use problem. Many programs write temporary files to publicly accessible directories. You can set the file permissions of the temporary file to prevent another user from altering the file. However, if the file already exists before you write to it, you could be overwriting data needed by another program or you could be using a file prepared by an attacker, in which case it might be a symbolic link, redirecting your output to a file needed by the system or to a file controlled by the attacker. To prevent this, programs often check to make sure a temporary file with a specific name does not already exist in the target directory, and then they open the file to write to it.

An attacker can create a race condition by repeatedly creating and removing files with the name used by your program for the temporary file. If they create the file at just the right moment, it will be after your program has checked to make sure the file doesn't exist, but before the program writes to it. Then, when your program does write to the temporary file, it will be writing to the attacker's file rather than creating a new file.

In a vulnerability in a directory server, a server script was executing commands to write private and public keys to temporary files, then reading those keys and putting them in a database. Because the temporary files were in a publicly writable directory, an attacker could have created a race condition by substituting the attacker's own files (or symbolic links to the attacker's files) before the keys were read, thus causing the script to read the attacker's private and public keys. After that, anything encrypted or authenticated using those keys would be under the attacker's control. Or the attacker could have read the private keys, which can be used to decrypt encrypted data. Private keys must be kept secret to be useful. [CVE-2005-2519]

Often, rather than substituting an ordinary file for your temporary file, an attacker creates a hard or symbolic link.

Here are some guidelines to help you avoid time of check–time of use vulnerabilities. For more detailed discussions, especially for C code, see Viega and McGraw, Building Secure Software, Addison Wesley, 2002, and Wheeler, Secure Programming for Linux and Unix HOWTO, available at http://www.dwheeler.com/secure-programs/.



< Previous PageNext Page > Hide TOC


© 2008 Apple Inc. All Rights Reserved. (Last updated: 2008-05-23)


Did this document help you?
Yes: Tell us what works for you.
It’s good, but: Report typos, inaccuracies, and so forth.
It wasn’t helpful: Tell us what would have helped.