fs-util: add racy RENAME_NOREPLACE fallback using access()

Apparently FAT on some recent kernels can't do RENAME_NOREPLACE, and of
course cannot do linkat()/unlinkat() either (as the hard link concept
does not exist on FAT). Add a fallback using an explicit beforehand
faccessat() check. This sucks, but what we can do if the safe operations
are not available?

Fixes: #10063
This commit is contained in:
Lennart Poettering 2018-10-02 13:34:18 +02:00
parent eaa680c09e
commit 2f15b6253a
1 changed files with 28 additions and 25 deletions

View File

@ -89,41 +89,44 @@ int rmdir_parents(const char *path, const char *stop) {
} }
int rename_noreplace(int olddirfd, const char *oldpath, int newdirfd, const char *newpath) { int rename_noreplace(int olddirfd, const char *oldpath, int newdirfd, const char *newpath) {
struct stat buf; int r;
int ret;
ret = renameat2(olddirfd, oldpath, newdirfd, newpath, RENAME_NOREPLACE); /* Try the ideal approach first */
if (ret >= 0) if (renameat2(olddirfd, oldpath, newdirfd, newpath, RENAME_NOREPLACE) >= 0)
return 0; return 0;
/* renameat2() exists since Linux 3.15, btrfs added support for it later. /* renameat2() exists since Linux 3.15, btrfs and FAT added support for it later. If it is not implemented,
* If it is not implemented, fallback to another method. */ * fall back to a different method. */
if (!IN_SET(errno, EINVAL, ENOSYS)) if (!IN_SET(errno, EINVAL, ENOSYS, ENOTTY))
return -errno; return -errno;
/* The link()/unlink() fallback does not work on directories. But /* Let's try to use linkat()+unlinkat() as fallback. This doesn't work on directories and on some file systems
* renameat() without RENAME_NOREPLACE gives the same semantics on * that do not support hard links (such as FAT, most prominently), but for files it's pretty close to what we
* directories, except when newpath is an *empty* directory. This is * want though not atomic (i.e. for a short period both the new and the old filename will exist). */
* good enough. */ if (linkat(olddirfd, oldpath, newdirfd, newpath, 0) >= 0) {
ret = fstatat(olddirfd, oldpath, &buf, AT_SYMLINK_NOFOLLOW);
if (ret >= 0 && S_ISDIR(buf.st_mode)) { if (unlinkat(olddirfd, oldpath, 0) < 0) {
ret = renameat(olddirfd, oldpath, newdirfd, newpath); r = -errno; /* Backup errno before the following unlinkat() alters it */
return ret >= 0 ? 0 : -errno; (void) unlinkat(newdirfd, newpath, 0);
return r;
}
return 0;
} }
/* If it is not a directory, use the link()/unlink() fallback. */ if (!IN_SET(errno, EINVAL, ENOSYS, ENOTTY, EPERM)) /* FAT returns EPERM on link()… */
ret = linkat(olddirfd, oldpath, newdirfd, newpath, 0);
if (ret < 0)
return -errno; return -errno;
ret = unlinkat(olddirfd, oldpath, 0); /* OK, neither RENAME_NOREPLACE nor linkat()+unlinkat() worked. Let's then fallback to the racy TOCTOU
if (ret < 0) { * vulnerable accessat(F_OK) check followed by classic, replacing renameat(), we have nothing better. */
/* backup errno before the following unlinkat() alters it */
ret = errno; if (faccessat(newdirfd, newpath, F_OK, AT_SYMLINK_NOFOLLOW) >= 0)
(void) unlinkat(newdirfd, newpath, 0); return -EEXIST;
errno = ret; if (errno != ENOENT)
return -errno;
if (renameat(olddirfd, oldpath, newdirfd, newpath) < 0)
return -errno; return -errno;
}
return 0; return 0;
} }