A TOCTOU (time-of-check, time-of-use) race condition is possible when two or more concurrent processes are operating on a shared file system [Seacord 2013b]. Typically, the first access is a check to verify some attribute of the file, followed by a call to use the file. An attacker can alter the file between the two accesses, or replace the file with a symbolic or hard link to a different file. These TOCTOU conditions can be exploited when a program performs two or more file operations on the same file name or path name.
A program that performs two or more file operations on a single file name or path name creates a race window between the two file operations. This race window comes from the assumption that the file name or path name refers to the same resource both times. If an attacker can modify the file, remove it, or replace it with a different file, then this assumption will not hold.
Noncompliant Code Example
If an existing file is opened for writing with the
w
mode argument, the file's previous contents (if any) are
destroyed. This noncompliant code example tries to prevent an existing
file from being overwritten by first opening it for reading before
opening it for writing. An attacker can exploit the race window
between the two calls to
fopen()
to overwrite an existing file.
#include <stdio.h>
void
open_some_file(
const
char
*file) {
FILE
*f =
fopen
(file,
"r"
);
if
(NULL != f) {
/* File exists, handle error
*/
}
else
{
if
(
fclose
(f) == EOF) {
/* Handle error */
}
f =
fopen
(file,
"w"
);
if
(NULL == f) {
/* Handle error */
}
/* Write to file */
if
(
fclose
(f) == EOF) {
/* Handle error */
}
}
}
|
Compliant Solution
This compliant solution invokes
fopen()
at a single location and uses the
x
mode of
fopen()
, which was added in C11. This mode causes
fopen()
to fail if the file exists. This check and subsequent open is
performed without creating a race window. The
x
mode provides exclusive access to the file only if the host
environment provides this support.
#include <stdio.h>
void
open_some_file(
const
char
*file) {
FILE
*f =
fopen
(file,
"wx"
);
if
(NULL == f) {
/* Handle error */
}
/* Write to file */
if
(
fclose
(f) == EOF) {
/* Handle error */
}
}
|
Compliant Solution (POSIX)
This compliant solution uses the
O_CREAT
and
O_EXCL
flags of POSIX's
open()
function. These flags cause
open()
to fail if the file exists.
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
void
open_some_file(
const
char
*file) {
int
fd = open(file, O_CREAT | O_EXCL
| O_WRONLY);
if
(-1 != fd) {
FILE
*f = fdopen(fd,
"w"
);
if
(NULL != f) {
/* Write to file */
if
(
fclose
(f) == EOF) {
/* Handle error */
}
}
else
{
if
(close(fd) == -1) {
/* Handle error */
}
}
}
}
|
Exceptions
FIO45-C-EX1: TOCTOU race conditions require that the vulnerable process is more privileged than the attacker; otherwise there is nothing to be gained from a successful attack. An unprivileged process is not subject to this rule.
FIO45-C-EX2: Accessing a file name or path name multiple times is permitted if the file referenced resides in a secure directory. (For more information, see FIO15-C. Ensure that file operations are performed in a secure directory.)
FIO45-C-EX3: Accessing a file name or path name multiple times is permitted if the program can verify that every operation operates on the same file.
This POSIX code example verifies that each subsequent file access
operates on the same file. In POSIX, every file can be uniquely
identified by using its device and i-node attributes. This code
example checks that a file name refers to a regular file (and not a
directory, symbolic link, or other special file) by invoking
lstat()
. This call also retrieves its device and i-node. The file is
subsequently opened. Finally, the program verifies that the file that
was opened is the same one (matching device and i-nodes) as the file
that was confirmed as a regular file.
#include
<sys/stat.h>
#include <fcntl.h>
int
open_regular_file(
char
*filename,
int
flags) {
struct
stat lstat_info;
struct
stat fstat_info;
int
f;
if
(lstat(filename,
&lstat_info) == -1) {
/* File does not exist,
handle error */
}
if
(!S_ISREG(lstat_info.st_mode)) {
/* File is not a regular
file, handle error */
}
if
((f = open(filename, flags)) ==
-1) {
/* File has disappeared,
handle error */
}
if
(fstat(f, &fstat_info) ==
-1) {
/* Handle error */
}
if
(lstat_info.st_ino !=
fstat_info.st_ino ||
lstat_info.st_dev !=
fstat_info.st_dev) {
/* Open file is not the
expected regular file, handle error */
}
/* f is the expected regular
open file */
return
f;
}
|