Original in fr Frédéric Raynal, Christophe Blaess, Christophe Grenier
fr to en Georges Tarbouriech
en to en Lorne Bailey
Christophe Blaess is an independent aeronautics engineer. He is a Linux fan and does much of his work on this system. He coordinates the translation of the man pages as published by the Linux Documentation Project.
Christophe Grenier is a 5th year student at the ESIEA, where he works as a sysadmin too. He has a passion for computer security.
Frédéric Raynal has been using Linux for many years because it doesn't pollute, doesn't use hormones, MSG or animal bone-meal... only sweat and craft.
The general principle defining race conditions is the following : a process wants to access a system resource exclusively. It checks that the resource is not already used by another process, then uses it as it pleases. The race condition occurs when another process tries to use the same resource in the time-lag between the first process checking that resource and actually taking it over. The side effects may vary. The classical case in OS theory is the deadlock of both processes. More often it leads to application malfunction or even to security holes when a process wrongfully benefits from the privileges another.
What we previously called a resource can have different aspects.
Most notably the race conditions discovered and corrected in the Linux kernel
itself due to competitive access to memory areas. Here, we will focus on
system applications and we'll deem that the concerned resources are
filesystem nodes. This concerns not only regular files but also direct access to
devices through special entry points from the /dev/
directory.
Most of the time, an attack aiming to compromise system security is done against
Set-UID applications since the attacker can
benefit from the privileges of the owner of the executable file.
However, unlike previously discussed security holes (buffer overflow, format
strings...), race conditions usually don't allow the
execution of "customized" code. Rather, they benefit from
the resources of a program while it's running.
This type of attack is also aimed at "normal" utilities
(not Set-UID), the cracker lying in ambush for another user,
especially root, to run the concerned application and access its
resources. This is also true for writing to a file
(i.e, ~/.rhost
in which the string
"+ +
" provides a direct access from any machine without
password), or for reading a confidential file (sensitive commercial data,
personal medical information, password file, private key...)
Unlike the security holes discussed in our previous articles, this security problem applies to every application and not just to Set-UID utilities and system servers or daemons.
Let's have a look at the behavior of a Set-UID program that needs to save data in a file belonging to the user. We could, for instance, consider the case of a mail transport software like sendmail. Let's suppose the user can both provide a backup filename and a message to write into that file, which is plausible under some circumstances. The application must then check if the file belongs to the person who started the program. It also will check that the file is not a symlink to a system file. Let's not forget, the program being Set-UID root, it is allowed to modify any file on the machine. Accordingly, it will compare the file's owner to its own real UID. Let's write something like :
1 /* ex_01.c */ 2 #include <stdio.h> 3 #include <stdlib.h> 4 #include <unistd.h> 5 #include <sys/stat.h> 6 #include <sys/types.h> 7 8 int 9 main (int argc, char * argv []) 10 { 11 struct stat st; 12 FILE * fp; 13 14 if (argc != 3) { 15 fprintf (stderr, "usage : %s file message\n", argv [0]); 16 exit(EXIT_FAILURE); 17 } 18 if (stat (argv [1], & st) < 0) { 19 fprintf (stderr, "can't find %s\n", argv [1]); 20 exit(EXIT_FAILURE); 21 } 22 if (st . st_uid != getuid ()) { 23 fprintf (stderr, "not the owner of %s \n", argv [1]); 24 exit(EXIT_FAILURE); 25 } 26 if (! S_ISREG (st . st_mode)) { 27 fprintf (stderr, "%s is not a normal file\n", argv[1]); 28 exit(EXIT_FAILURE); 29 } 30 31 if ((fp = fopen (argv [1], "w")) == NULL) { 32 fprintf (stderr, "Can't open\n"); 33 exit(EXIT_FAILURE); 34 } 35 fprintf (fp, "%s\n", argv [2]); 36 fclose (fp); 37 fprintf (stderr, "Write Ok\n"); 38 exit(EXIT_SUCCESS); 39 }
As we explained in our first article, it would be better for a Set-UID application to temporarily drop its privileges and open the file using the real UID of the user having called it. As a matter of fact, the above situation corresponds to a daemon, providing services to every user. Always running under the root ID, it would check using the UID instead of its own real UID. Nevertheless, we'll keep this scheme for now, even if it isn't that realistic, since it allows us to understand the problem while easily "exploiting" the security hole.
As we can see, the program starts doing all the needed checks, i.e. that
the file exists, that it belongs to the user and that it's a normal file.
Next, it actually opens the file and writes the message. That is where
the security hole lies! Or, more exactly, it's within the lapse of time between the
reading of the file attributes with stat()
and its opening with
fopen()
. This lapse of time is often extremely short but
an attacker can benefit from it to change the file's characteristics.
To make our attack even easier, let's add a line that causes the
process to sleep
between the two operations, thus having the time to do the job by hand. Let's
change the line 30 (previously empty) and insert :
30 sleep (20);
Now, let's implement it; first, let's make the application
Set-UID root. Let's make, it's very
important, a backup copy of our password file
/etc/shadow
:
$ cc ex_01.c -Wall -o ex_01 $ su Password: # cp /etc/shadow /etc/shadow.bak # chown root.root ex_01 # chmod +s ex_01 # exit $ ls -l ex_01 -rwsrwsr-x 1 root root 15454 Jan 30 14:14 ex_01 $
Everything is ready for the attack. We are in a directory belonging to us. We
have a Set-UID root utility (here
ex_01
) holding a security hole, and we feel like replacing the line
concerning root from the
/etc/shadow
password file with a line containing an empty password.
First, we create a fic
file belonging to us :
$ rm -f fic $ touch fic
Next, we run our application in the background "to keep the lead". We ask it to write a string into that file. It checks what it has to, sleeps for a while before really accessing the file.
$ ./ex_01 fic "root::1:99999:::::" & [1] 4426
The content of the root
line comes from the shadow(5)
man page, the most important being the empty second field (no password).
While the process is asleep, we have about 20 seconds to remove the
fic
file and replace it with a link
(symbolic or physical, both work) to the /etc/shadow
file.
Let's remember, that every user can create a link to a file
in a directory belonging to him even if he can't read the content,
(or in /tmp
, as we'll see a bit later). However it isn't possible to create a
copy of such a file, since it would require a full read.
$ rm -f fic $ ln -s /etc/shadow ./fic
Then we ask the shell to bring the ex_01
process back to the
foreground with the fg
command, and wait till it finishes :
$ fg ./ex_01 fic "root::1:99999:::::" Write Ok $
Voil� ! It's over, the
/etc/shadow
file only holds one line indicating root has
no password. You don't believe it ?
$ su # whoami root # cat /etc/shadow root::1:99999::::: #
Let's finish our experiment by putting the old password file back :
# cp /etc/shadow.bak /etc/shadow cp: replace `/etc/shadow'? y #
We succeeded in exploiting a race condition in a Set-UID root utility. Of course, this program was very "helpful" waiting for 20 seconds giving us time to modify the files behind its back. Within a real application, the race condition only applies for a very short time. How do we take advantage of that ?
Usually, the cracker relies on a brute force attack, renewing the attempts hundreds, thousands or ten thousand times, using scripts to automate the sequence. It's possible to improve the chance of "falling" into the security hole with various tricks aiming at increasing the lapse of time between the two operations that the program wrongly considers as atomically linked. The idea is to slow down the target process to manage the delay preceding the file modification more easily. Different approaches can help us to reach our goal :
nice -n 20
prefix;
while (1);
);
The method allowing us to benefit from a security hole based in race condition is boring and repetitive, but it really is usable ! Let's try to find the most effective solutions.
The problem discussed above relies on the ability to change an object's
characteristics during the time-lapse between two operations,
the whole thing being as continuous as possible. In the previous
situation, the change did not concern the file itself. By the way, as a normal
user it would have been quite difficult to modify, or even to read, the
/etc/shadow
file. As a matter of fact, the change relies on the
link between the existing file node in the name tree and the file itself as a
physical entity. Let's remember most of the system commands
(rm
, mv
, ln
, etc.) act on the file name
not on the file content. Even when you delete a file (using rm
and
the unlink()
system call), the content is really deleted when the
last physical link - the last reference - is removed.
The mistake made in the previous program is considering
the association between the name of the file and its content as unchangeable,
or at least constant, during the lapse of time between stat()
and
fopen()
operation.
Thus, the example of a physical link should suffice to verify that this association is
not a permanent one at all. Let's take an example using this type of link. In a
directory belonging to us, we create a new link to a system file. Of course, the file's
owner and the access mode are kept. The ln
command -f
option forces the creation, even if that name already exists :
$ ln -f /etc/fstab ./myfile $ ls -il /etc/fstab myfile 8570 -rw-r--r-- 2 root root 716 Jan 25 19:07 /etc/fstab 8570 -rw-r--r-- 2 root root 716 Jan 25 19:07 myfile $ cat myfile /dev/hda5 / ext2 defaults,mand 1 1 /dev/hda6 swap swap defaults 0 0 /dev/fd0 /mnt/floppy vfat noauto,user 0 0 /dev/hdc /mnt/cdrom iso9660 noauto,ro,user 0 0 /dev/hda1 /mnt/dos vfat noauto,user 0 0 /dev/hda7 /mnt/audio vfat noauto,user 0 0 /dev/hda8 /home/ccb/annexe ext2 noauto,user 0 0 none /dev/pts devpts gid=5,mode=620 0 0 none /proc proc defaults 0 0 $ ln -f /etc/host.conf ./myfile $ ls -il /etc/host.conf myfile 8198 -rw-r--r-- 2 root root 26 Mar 11 2000 /etc/host.conf 8198 -rw-r--r-- 2 root root 26 Mar 11 2000 myfile $ cat myfile order hosts,bind multi on $
The /bin/ls
-i
option displays the inode
number at the beginning of the line. We can see the same name points to two
different physical inodes.
In fact, we would like the functions that check and access the file to
always point to the same content and the same inode. And it's possible !
The kernel itself automatically manages this association when it provides us
with a file descriptor. When we open a file for reading, the open()
system call returns an integer value, that is the descriptor, associating it with
the physical file by an internal table. All the reading we'll do next will
concern this file content, no matter what happens to the name used during the file
open operation.
Let's emphasize that point : once a file has been opened, every operation
on the filename, including removing it, will have no effect on the file
content. As long as there is still a process holding a descriptor for a file, the
file content isn't removed from the disk, even if its name disappears from the
directory where it was stored. The kernel maintains the association to
the file content between the open()
system
call providing a file descriptor and the release of this descriptor by
close()
or the process ends.
So there we have our solution ! We can open the file and then check
the permissions by examining the descriptor characteristics instead of the filename
ones. This is done using the fstat()
system call (this last working
like stat()
), but checking a file descriptor rather than a path.
To access the content of the file using the descriptor we'll use the fdopen()
function (that works like fopen()
) while relying on a descriptor
rather than on a filename. Thus, the program becomes :
1 /* ex_02.c */ 2 #include <fcntl.h> 3 #include <stdio.h> 4 #include <stdlib.h> 5 #include <unistd.h> 6 #include <sys/stat.h> 7 #include <sys/types.h> 8 9 int 10 main (int argc, char * argv []) 11 { 12 struct stat st; 13 int fd; 14 FILE * fp; 15 16 if (argc != 3) { 17 fprintf (stderr, "usage : %s file message\n", argv [0]); 18 exit(EXIT_FAILURE); 19 } 20 if ((fd = open (argv [1], O_WRONLY, 0)) < 0) { 21 fprintf (stderr, "Can't open %s\n", argv [1]); 22 exit(EXIT_FAILURE); 23 } 24 fstat (fd, & st); 25 if (st . st_uid != getuid ()) { 26 fprintf (stderr, "%s not owner !\n", argv [1]); 27 exit(EXIT_FAILURE); 28 } 29 if (! S_ISREG (st . st_mode)) { 30 fprintf (stderr, "%s not a normal file\n", argv[1]); 31 exit(EXIT_FAILURE); 32 } 33 if ((fp = fdopen (fd, "w")) == NULL) { 34 fprintf (stderr, "Can't open\n"); 35 exit(EXIT_FAILURE); 36 } 37 fprintf (fp, "%s", argv [2]); 38 fclose (fp); 39 fprintf (stderr, "Write Ok\n"); 40 exit(EXIT_SUCCESS); 41 }
This time, after line 20, no change to the filename (deleting, renaming, linking) will affect our program's behavior; the content of the original physical file will be kept.
When manipulating a file it's important to ensure the association between the internal representation and the real content stays constant. Preferably, we'll use the following system calls to manipulate the physical file as an already open descriptor rather than their equivalents using the path to the file :
System call | Use |
fchdir (int fd) |
Goes to the directory represented by fd. |
fchmod (int fd, mode_t mode) |
Changes the file access rights. |
fchown (int fd, uid_t uid, gid_t gif) |
Changes the file owner. |
fstat (int fd, struct stat * st) |
Consults the informations stored within the inode of the physical file. |
ftruncate (int fd, off_t length) |
Truncates an existing file. |
fdopen (int fd, char * mode) |
Initializes IO from an already open descriptor. It's an stdio library routine, not a system call. |
Then, of course, you must open the file in the wanted mode, calling
open()
(don't forget the third argument when creating a new file).
More on open()
later when we discuss the temporary file
problem.
We must insist that it is important to check the system calls return codes.
For instance, let's mention, even if it has nothing to do with
race conditions, a problem found in old /bin/login
implementations because it neglected an error code check. This application,
automatically provided a root access when not finding the
/etc/passwd
file. This behavior can seem acceptable as soon as a
damaged file system repair is concerned. On the other hand, checking that it was
impossible to open the file instead of checking if the file really existed, was
less acceptable. Calling /bin/login
after opening the
maximum number of allowed descriptors allowed any user to get
root access... Let's finish with this digression insisting in how it's
important to check, not only the system call's success or failure, but the error
codes too, before taking any action about system security.
A program dealing with system security shouldn't rely on the exclusive access to a file content. More exactly, it's important to properly manage the risks of race conditions to the same file. The main danger comes from a user running multiple instances of a Set-UID root application simultaneously or establishing multiple connections at once with the same daemon, hoping to create a race condition situation, during which the content of a system file could be modified in an unusual way.
To avoid a program being sensitive to this kind of situation, it's necessary to institute an exclusive access mechanism to the file data. This is the same problem as the one found in databases when various users are allowed to simultaneously query or change the content of a file. The principle of file locking solves this problem.
When a process wants to write into a file, it asks the kernel to lock that file - or a part of it. As long as the process keeps the lock, no other process can ask to lock the same file, or at least the same part of the file. In the same way, a process asks for a lock before reading the file content to ensure no changes will be made while it holds the lock.
As a matter of fact, the system is more clever than that : the kernel distinguishes between the locks required for file reading and those for file writing. Various processes can hold a lock for reading simultaneously since no one will attempt to change the file content. However, only one process can hold a lock for writing at a given time, and no other lock can be provided at the same time, even for reading.
There are two types of locks (mostly incompatible with each other). The first one
comes from BSD and relies on the flock()
system call. Its first
argument is the descriptor of the file you wish to access in an exclusive way,
and the second one is a symbolic constant representing the operation to be done.
It can have different values : LOCK_SH
(lock for reading),
LOCK_EX
(for writing), LOCK_UN
(release of the lock).
The system call blocks as long as the requested operation remains
impossible. However, you can do a binary OR |
of the
LOCK_NB
constant for the call to fail instead of staying locked.
The second type of lock comes from System V, and relies on the
fcntl()
system call whose invocation is a bit complicated.
There's a library function called lockf()
close to the system call
but not as fast. fcntl()
's first argument is the descriptor
of the file to lock. The second one represents the operation to be performed :
F_SETLK
and F_SETLKW
manage a lock, the second command
stays blocks till the operation becomes possible, while the first immediately
returns in case of failure.
F_GETLK
consults the lock state of a file (which is useless
for current applications). The third argument is a pointer to a variable of
struct flock
type, describing the lock.
The flock
structure important members are the following :
Name | Type | Meaning |
l_type |
int |
Expected action :
F_RDLCK (to lock for reading),
F_WRLCK (to lock for writing) and
F_UNLCK (to release the lock). |
l_whence |
int |
l_start Field origin (usually
SEEK_SET ). |
l_start |
off_t |
Position of the beginning of the lock (usually 0). |
l_len |
off_t |
Length of the lock, 0 to reach the end of the file. |
We can see fcntl()
can lock limited portions of the file, but it's
able to do much more compared to flock()
. Let's have a look at a
small program asking for a lock for reading concerning files which names are
given as an argument, and waiting for the user to press the Enter key before
finishing (and thus releasing the locks).
1 /* ex_03.c */ 2 #include <fcntl.h> 3 #include <stdio.h> 4 #include <stdlib.h> 5 #include <sys/stat.h> 6 #include <sys/types.h> 7 #include <unistd.h> 8 9 int 10 main (int argc, char * argv []) 11 { 12 int i; 13 int fd; 14 char buffer [2]; 15 struct flock lock; 16 17 for (i = 1; i < argc; i ++) { 18 fd = open (argv [i], O_RDWR | O_CREAT, 0644); 19 if (fd < 0) { 20 fprintf (stderr, "Can't open %s\n", argv [i]); 21 exit(EXIT_FAILURE); 22 } 23 lock . l_type = F_WRLCK; 24 lock . l_whence = SEEK_SET; 25 lock . l_start = 0; 26 lock . l_len = 0; 27 if (fcntl (fd, F_SETLK, & lock) < 0) { 28 fprintf (stderr, "Can't lock %s\n", argv [i]); 29 exit(EXIT_FAILURE); 30 } 31 } 32 fprintf (stdout, "Press Enter to release the lock(s)\n"); 33 fgets (buffer, 2, stdin); 34 exit(EXIT_SUCCESS); 35 }
We first launch this program from a first console where it waits :
$ cc -Wall ex_03.c -o ex_03 $ ./ex_03 myfile Press Enter to release the lock(s)>From another terminal...
$ ./ex_03 myfile Can't lock myfile $Pressing
Enter
in the first console, we release the locks.
With this locking mechanism, you can prevent race conditions to directories
and print queues, like the lpd
daemon, using a
flock()
lock on the /var/lock/subsys/lpd
file,
thus allowing only one instance.
You can also manage the access to a system file in a secure way like
/etc/passwd
, locked using fcntl()
from the
pam library when changing a user's data.
However, this only protects from interferences with applications having correct behavior, that is, asking the kernel to reserve the proper access before reading or writing to an important system file. We now talk about cooperative lock, what shows the application liability towards data access. Unfortunately, a badly written program is able to replace file content, even if another process, with good behavior, has a lock for writing. Here is an example. We write a few letters into a file and lock it using the previous program :
$ echo "FIRST" > myfile $ ./ex_03 myfile Press Enter to release the lock(s)>From another console, we can change the file :
$ echo "SECOND" > myfile $Back to the first console, we check the "damages" :
(Enter) $ cat myfile SECOND $
To solve this problem, the Linux kernel provides the sysadmin with a locking
mechanism coming from System V. Therefore you can only use it with
fcntl()
locks and not with flock()
.
The administrator can tell the kernel the fcntl()
locks are
strict, using a particular combination of access rights.
Then, if a process locks a file for writing, another process won't be able to
write into that file (even as root).
The particular combination is to use the Set-GID bit while the
execution bit is removed for the group. This is obtained with the command :
$ chmod g+s-x myfile $However this is not enough. For a file to automatically benefit from strict cooperative locks, the mandatory attribute must be activated on the partition where it can be found. Usually, you have to change the
/etc/fstab
file to add the mand
option in the 4th
column, or typing the command :
# mount /dev/hda5 on / type ext2 (rw) [...] # mount / -o remount,mand # mount /dev/hda5 on / type ext2 (rw,mand) [...] #Now, we can check that a change from another console is impossible :
$ ./ex_03 myfile Press Enter to release the lock(s)>From another terminal :
$ echo "THIRD" > myfile bash: myfile: Resource temporarily not available $And back to the first console :
(Enter) $ cat myfile SECOND $
The administrator and not the programmer has to decide to make strict file locks
(for instance /etc/passwd
, or /etc/shadow
). The
programmer has to control the way the data is accessed, what ensures his
application to manages data coherently when reading and it is not dangerous for other
processes when writing, as long as the environment is properly administrated.
Very often a program needs to temporarily store data in an external file. The
most usual case is inserting a record in the middle of a sequential ordered
file, which implies that we make a copy of the original file in a temporary file,
while adding new information. Next the unlink()
system call removes
the original file and rename()
renames the temporary file to
replace the previous one.
Opening a temporary file, if not done properly, is often the starting point of race condition situations for an ill-intentioned user. Security holes based on the temporary files have been recently discovered in applications such as Apache, Linuxconf, getty_ps, wu-ftpd, rdist, gpm, inn, etc. Let's remember a few principles to avoid this sort of trouble.
Usually, temporary file creation is done in the /tmp
directory.
This allows the sysadmin to know where short term data storage is done. Thus, it's
also possible to program a periodic cleaning (using cron
), the use
of an independent partition formated at boot time, etc.
Usually, the administrator defines the location reserved for temporary files in
the <paths.h
> and <stdio.h
> files, in the
_PATH_TMP
and P_tmpdir
symbolic constants definition.
As a matter of fact, using another default directory than /tmp
is
not that good, since it would imply recompiling every application, including the
C library. However, let's mention that GlibC routine behavior can be defined
using the TMPDIR
environment variable.
Thus, the user can ask the temporary files to be stored in a directory belonging
to him rather than in /tmp
. This is sometimes mandatory when the
partition dedicated to /tmp
is too small to run applications
requiring big amount of temporary storage.
The /tmp
system directory is something special because of its
access rights :
$ ls -ld /tmp drwxrwxrwt 7 root root 31744 Feb 14 09:47 /tmp $
The Sticky-Bit represented by the letter t
at the end or
the 01000 octal mode, has a particular meaning when applied to a directory :
only the directory owner (root ), and the owner of a file found in that
directory are able to delete the file. The directory having a full write access,
each user can put his files in it, being sure they are protected - at least till
the next clean up managed by the sysadmin.
Nevertheless, using the temporary storage directory may cause a few problems.
Let's start with the trivial case, a Set-UID root application talking
to a user. Let's talk about a mail transport program. If this process receives
a signal asking it to finish immediately, for instance
SIGTERM or SIGQUIT during a system shutdown, it can
try to save on the fly the mail already written but not sent. With old versions,
this was done in /tmp/dead.letter
. Then, the user just had to
create (since he can write into /tmp
) a physical link to /etc/passwd
with the name dead.letter
for the mailer
(running under effective UID root) to write to this file the content of
the not yet finished mail (incidently containing a line
"root::1:99999:::::
").
The first problem with this behavior is the foreseeable nature of the filename.
You can to watch such an application only once to deduct it will use the
/tmp/dead.letter
file name. Therefore, the first step is to use a
filename defined for the current program instance. There are various library
functions able to provide us with a personal temporary filename.
Let's suppose we have such a function providing a unique name for our temporary file. Free software being available with source code (and so for C library), the filename is however foreseeable even if it's rather difficult. An attacker could create a symlink to the name provided by the C library. Our first reaction is to check the file exists before opening it. Naively we could write something like :
if ((fd = open (filename, O_RDWR)) != -1) { fprintf (stderr, "%s already exists\n", filename); exit(EXIT_FAILURE); } fd = open (filename, O_RDWR | O_CREAT, 0644); ...
Obviously, this is a typical case of race condition,
where a security hole opens following the action from a user succeeding in
creating a link to /etc/passwd
between the first
open()
and the second one.
These two operations have to be done in an atomic way, without any manipulation
able to take place between them. This is possible using a specific option of the
open()
system call. Called O_EXCL, and used
in conjunction with O_CREAT, this option makes the
open() fail if the file already exists, but the check of existence is
atomically linked to the creation.
By the way, the 'x
' Gnu extension for the opening modes of the
fopen()
function, requires an exclusive file creation, failing if
the file already exists :
FILE * fp; if ((fp = fopen (filename, "r+x")) == NULL) { perror ("Can't create the file."); exit (EXIT_FAILURE); }
The temporary files permissions are quite important too. If you have to write confidential information into a mode 644 file (read/write for the owner, read only for the rest of the world) it can be a bit of a nuisance. The
#include <sys/types.h> #include <sys/stat.h> mode_t umask(mode_t mask);function allows us to determine the permissions of a file at creation time. Thus, following a
umask(077)
call, the file will be open in mode 600
(read/write for the owner, no rights at all for the others).
Usually, the temporary file creation is done in three steps :
O_CREAT | O_EXCL
, with the most
restrictive permissions;
How create a temporary file ? The
#include <stdio.h> char *tmpnam(char *s); char *tempnam(const char *dir, const char *prefix);functions return pointers to randomly created names.
The first function accepts a NULL
argument, then it returns a
static buffer address. Its content will change at
tmpnam(NULL)
next call. If the argument is an allocated string, the
name is copied there, what requires a string of at least
L-tmpnam
bytes. Be careful with buffer overflows !
The man
page informs about problems when the function is used with
a NULL
parameter, if _POSIX_THREADS
or
_POSIX_THREAD_SAFE_FUNCTIONS
are defined.
The tempnam()
function returns a pointer to a string. The
dir
directory must be "suitable" (the
man
page describes the right meaning of "suitable"). This function
checks the file doesn't exist before returning its name. However, once again,
the man
page doesn't recommend its use, since "suitable" can have a
different meaning according to the function implementations.
Let's mention that Gnome recommends its use in this way :
char *filename; int fd; do { filename = tempnam (NULL, "foo"); fd = open (filename, O_CREAT | O_EXCL | O_TRUNC | O_RDWR, 0600); free (filename); } while (fd == -1);The loop used here, reduces the risks but creates new ones. What would happen if the partition where you want to create the temporary file is full, or if the system already opened the maximum number of files available at once...
The
#include <stdio.h> FILE *tmpfile (void);function creates an unique filename and opens it. This file is automatically deleted at closing time.
With GlibC-2.1.3, this function uses a mechanism similar to
tmpnam()
to generate the filename, and opens the corresponding
descriptor. The file is then deleted, but Linux really removes it when no
resources at all use it, that is when the file descriptor is released, using a
close()
system call.
FILE * fp_tmp; if ((fp_tmp = tmpfile()) == NULL) { fprintf (stderr, "Can't create a temporary file\n"); exit (EXIT_FAILURE); } /* ... use of the temporary file ... */ fclose (fp_tmp); /* real deletion from the system */
The simplest cases don't require filename change nor transmission to
another process, but only storage and data re-reading in a temporary area.
We therefore don't need to know the name of the temporary file but only to
access its content.
The tmpfile()
function does it.
The man
page says nothing, but the Secure-Programs-HOWTO doesn't
recommend it. According to the author, the specifications don't guarantee the
file creation and he hasn't been able to check every implementation. Despite this
reserve, this function is the most efficient.
Last, the
#include <stdlib.h> char *mktemp(char *template); int mkstemp(char *template);functions create an unique name from a template made of a string ending with "
XXXXXX
". These 'X's are replaced to get an unique filename.
According to versions, mktemp()
replaces the first five
'X' with the Process ID (PID) ... what makes the name rather easy to
guess : only the last 'X' is random. Some versions allow more than six 'X'.
mkstemp()
is the recommended function in the
Secure-Programs-HOWTO. Here is the method :
#include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <sys/stat.h> void failure(msg) { fprintf(stderr, "%s\n", msg); exit(1); } /* * Creates a temporary file and returns it. * This routine removes the filename from the filesystem thus * it doesn't appear anymore when listing the directory. */ FILE *create_tempfile(char *temp_filename_pattern) { int temp_fd; mode_t old_mode; FILE *temp_file; /* Create file with restrictive permissions */ old_mode = umask(077); temp_fd = mkstemp(temp_filename_pattern); (void) umask(old_mode); if (temp_fd == -1) { failure("Couldn't open temporary file"); } if (!(temp_file = fdopen(temp_fd, "w+b"))) { failure("Couldn't create temporary file's file descriptor"); } if (unlink(temp_filename_pattern) == -1) { failure("Couldn't unlink temporary file"); } return temp_file; }
These functions show the problems concerning abstraction and portability. That
is, the standard library functions are expected to provide features
(abstraction)... but the way to implement them varies according to the system
(portability).
For instance, the tmpfile()
function opens a temporary file
in different ways (some versions don't use O_EXCL
),
or mkstemp()
handles a variable number of 'X' according to
implementations.
We flew over most of the security problems concerning race conditions to the same
resource. Let's remember you must never assume that two consecutive operations are
always sequentially processed in the CPU unless the kernel manages this. If race conditions generate
security holes, you must not neglect the holes caused by relying on other resources, such
as variables shared between threads or memory segments shared using
shmget()
. Selection access mechanisms (semaphore, for
example) must be used to avoid hard to discover bugs.