When we write code, we often assume that our program will run continuously
until the program terminates. In fact this is rarely the case. On most
modern machines, the operating system multi-tasks, which is a fancy way
of saying appears to run many things at the same time. Since single-CPU
machines can only really run a single process at a time, individual programs are
paused and restarted many times during their invocation. Fortunately this
typically happens so fast that it is rarely noticeable to the user.

The effects of multi-tasking can mean that your system state can
change unexpectedly between one statement and the next. For example,
imagine that one code statement tests that a certain file exists, and
the next statement executes code to open this file. If the program is
paused by the operating system before executing the second statement,
then the file could be deleted or moved by another program, and the
assumption that the file exists is now false. This is called a race
condition.

At the best race conditions can result in unexpected and difficult
to debug behaviour from your programs. Usually they work fine, but
sometimes they will have problems. Two processes may start writing to the
same file, resulting in data corruption, or two processes may start
processing the same information, resulting in duplicated work. At
their worst, race-conditions can result in security holes on your
system. There's a long tradition of attackers using name-guessing
and symbolic links to trick privileged programs into opening and
potentially rewriting critical files.

Unfortunately, it's easy to fall prey to race conditions -- they're
not always obvious, and they are rarely if ever detected during testing.
In fact, Perl comes with a set of operators that are so often
associated with race conditions that they are sometimes referred
to as the 'garden path' of race conditions. These are Perl's
file test operators.

The file test operators are extremely easy to use, and by themselves
they present no problems. However, file test operators are commonly
used to decide whether or not a file should be opened:

The above example contains a race condition. At the time of the
exists test (-e), the file may well not exist. However, by the
time we reach the open another process may have created it. Our
program would then destroy the contents of that file, which may be in
use by another instance of the same program. If our program
was running with privileges, this code could potentially chase a symbolic
link (a type of shortcut) and destroy a critical file on our system.

This code provides a false sense of security, as it appears that we're
taking measures against accidental overwriting of files, but in
reality we're still vulnerable. Due to the difficulty of reproducing
the race condition, it can be extremely difficult to diagnose and
debug.

This doesn't mean that you can't use file-test operators when coding
securely, but you must be fully aware that files can change in between
the testing of a file and any other operation which you may perform
upon it.

Avoiding the need for file-test operators

We used a file-test operator to test the existence of a file before opening
it, but as we've just seen this is a mistake. Not only can this result in
a race condition, but sometimes it's also unnecessary. If all we want is
to check file access permissions, then we should just go ahead and try to
open the file. If we don't have permission, this operation will result
in failure.

To avoid the more difficult problems, such as following
symbolic-links, or truncating files which already exist, Perl gives us
the sysopen command. This allows us to specify conditions on the
file at the same time as we access it. The atomicity of this test
and open is guaranteed, thus race conditions are avoided.

Here are some examples of using sysopen and their test/open equivalent:

use Fcntl;
# Open file for writing, only if it doesn't already exist.
# This also avoids chasing symbolic links on Unix systems.
sysopen(my $fh, $outfile, O_WRONLY|O_CREAT|O_EXCL)
or die "Failed to open $outfile: $!";
# equivalent to (race condition version)
if (-e $outfile) {
die "$outfile already exists on system";
}
open(my $fh, ">", $outfile)
or die "Failed to open $outfile: $!";
# Open file for writing, create it if does not exist, truncate if
# it does, and do not follow symbolic links. Not all
# operating systems support the O_NOFOLLOW option.
sysopen(my $fh, $outfile, O_WRONLY|O_CREAT|O_TRUNC|O_NOFOLLOW)
or die "Failed to open $outfile: $!";
# equivalent to (race condition version)
if (-l $outfile) {
die "$outfile is a sym-link";
}
open(my $fh, ">", $outfile)
or die "Failed to open $outfile: $!";

The sysopen function allows many other combinations of options for file
access, and should be considered whenever you need fine-grained
control over how a file is opened. To find out more, read
perldoc -f sysopen.