Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to rename() without race conditions?

If I want to rename A to B, but only if B doesn't exist, the naive thing would be checking if B exists (with access("B", F_OK) or something like that), and if it doesn't proceeding with rename. Unfortunately this opens a window during which some other process might decide to create B, and then it gets overwritten - and even worse there's no indication that something like that ever happened.

Other file system access functions don't suffer from this - open has O_EXCL (so copying files is safe), and recently Linux got an entire family of *at syscalls that protect against most other race conditions - but not this particular one (renameat exists, but protects against an entirely different problem).

So does it have a solution?

like image 265
taw Avatar asked Jul 11 '10 08:07

taw


4 Answers

As of Linux kernel 3.15 (released in June 2014), this can be done with syscall(__NR_renameat2, AT_FDCWD, "source-file", AT_FDCWD, "dest-file", RENAME_NOREPLACE) (include <syscall.h>, <fcntl.h> and <linux/fs.h>).

This is better than link(), because there is never a point where both filenames exist simultaneously (in particular, with link(), a precisely-timed power outage could cause both names to remain forever).

glibc 2.28 (released in August 2018) adds a renameat2() wrapper, so you can use that instead of syscall.h and linux/fs.h (though you'll most likely need <stdio.h> and #define __GNU_SOURCE instead).

For more details, see http://man7.org/linux/man-pages/man2/rename.2.html (though it does, as of writing, not know that glibc now has a renameat2 wrapper).

like image 150
Alcaro Avatar answered Oct 18 '22 16:10

Alcaro


You should be able to link(2) to the new file name. If the link fails then you give up because the file already exists. If the link succeeds, your file now exists under both the old and the new name. Then you unlink(2) the old name. No possible race condition.

like image 22
rettops Avatar answered Oct 31 '22 14:10

rettops


You could link() to the existing file with the new filename you want, then remove the existing filename.

link() should succeed in creating a new link only if the new pathname doesn't already exist.

Something like:

int result = link( "A", "B");

if (result != 0) {
    // the link wasn't created for some reason (maybe because "B" already existed)
    // handle the failure however appropriate...
    return -1;
}

// at this point there are 2 filenames hardlinked to the contents of "A", 
//   filename "A" and filename "B"

// remove filename "A"
unlink( "A");

This technique is discussed in the docs for link() (see the discussion about modifying the passwd file):

  • http://www.opengroup.org/onlinepubs/009695399/functions/link.html
like image 7
Michael Burr Avatar answered Oct 31 '22 14:10

Michael Burr


Sorry for adding something to an old thread. And for doing such a long post.

I only know one single way to do a complete race condition free rename() in absence of locking which should virtually work on any filesystem, even on NFS with intermittend server reboots and client time warps in place.

The following recipe is race condition free in the sense that at no circumstance data can get lost. It also does not need locks and can be performed by clients which do not want to cooperate except that they all use the same algorithm.

It is not race condition free in the sense that, if something seriously breaks, everything is left in a clean an tidy state. It also has a short period of time, where neither the source nor the destination are present at their location, however the source still is around under another name. And it is not hardened against cases where an attacker tries to provoke harm (the rename() is the culprit, go figure).

S is the Source, D is the destination, P(x) is dirname(x), C(x,y) is x/y path concatenation

  1. check that the destination does not exist. Just to make sure we do not do the next steps in vain.
  2. create a probably unique name T := C(P(D),random)
  3. mkdir(T), if this fails loop to previous step
  4. open(C(T,"lock"),O_EXCL), if this fails rmdir(T) ignoring errors and loop to previous step
  5. rename(S,C(T,"tmp"))
  6. link(C(T,"tmp"),D)
  7. unlink(C(T,"tmp"))
  8. unlink(C(T,"lock"))
  9. rmdir(T)

Algorithm safe_rename(S,D) explained:

The problem is that we want to make sure there is no race condition, neither on the source nor on the destination. It is assumed, that (nearly) anything can happen between each step, but all other processes follow the exact same algorithm when doing race condition free renames. This includes that the temporary directories T are never touched, except after making sure (this is a manual process) that the process using the directory has died and cannot be resurrected (like continuing a VM hibernate after a restore).

To properly do the rename(), we need some place to hide away. So we construct a directory a way which makes sure that nobody else (who is following the same algorithm) accidentally will use it.

However mkdir() is not guaranteed to be atomic on NFS. Hence we need to make sure that we have some guarantee that we are alone in the directory. This is O_EXCL on the lockfile. This is - strictly speaking - not locking, it is a semaphore.

Except from such rare cases, mkdir() usually is atomic. Also we can create use some cryptographically secure random name for the directory, add some GUID, hostname and PID to make sure it is very unlikely that somebody else chooses the same name by chance. However to proof the algorithm is correct we need this file named lock.

Now that we have a mostly empty directory, we can safely rename() the source there. This ensures that nobody else alters the source until we will unlink() it. (Well, contents can change, this is not a problem.)

Now the link() trick can be applied to make sure we do not overwrite the destination.

Afterwards the unlink() can be done race condition free on the remaining source. The rest is cleanup.

There is only one problem left:

In case the link() fails we have moved the source already. For proper cleanup we need to move it back. This can be done by calling safe_rename(C(T,"tmp"),S). If this fails, too, all we can do is to try to cleanup as much as we can (unlink(C(T,"lock")), rmdir(T)) and leave the debris behind for manual cleanup by the admin.

Final notes:

To help to clean up in the debris case, you can possibly use some better filename than tmp. Choosing names cleverly can somewhat harden the algorithm against attacks as well.

And if you are moving trainloads of files somewhere you can reuse the directory of course.

However, I agree, that this algorithm is plain overkill and something like O_EXCL on rename() is missing.

like image 3
Tino Avatar answered Oct 31 '22 14:10

Tino