From 76df06ff8fa39ae0cb0d167b7f622139778dc7d7 Mon Sep 17 00:00:00 2001 From: Kamil Dudka Date: Thu, 4 Jan 2018 09:42:10 +0100 Subject: [PATCH] mv -n: do not overwrite the destination ... if it is created by another process after mv has checked its non-existence. * src/copy.c (copy_internal): Use renameat2 (..., RENAME_NOREPLACE) if called by mv -n. If it fails with EEXIST in that case, pretend successful rename as if the existing destination file was detected by the preceding lstat call. Fixes https://bugs.gnu.org/29961 --- aclocal.m4 | 1 + bootstrap.conf | 2 + gnulib-tests/gnulib.mk | 18 ++++ gnulib-tests/test-renameat.c | 206 ++++++++++++++++++++++++++++++++++++++ gnulib-tests/test-renameat2.c | 209 ++++++++++++++++++++++++++++++++++++++ lib/gnulib.mk | 21 +++- lib/renameat.c | 25 +++++ lib/renameat2.c | 227 ++++++++++++++++++++++++++++++++++++++++++ lib/renameat2.h | 30 ++++++ m4/gnulib-comp.m4 | 22 ++++ m4/renameat.m4 | 25 +++++ src/copy.c | 27 ++++- 12 files changed, 808 insertions(+), 5 deletions(-) create mode 100644 gnulib-tests/test-renameat.c create mode 100644 gnulib-tests/test-renameat2.c create mode 100644 lib/renameat.c create mode 100644 lib/renameat2.c create mode 100644 lib/renameat2.h create mode 100644 m4/renameat.m4 diff --git a/aclocal.m4 b/aclocal.m4 index 9c5a2b0..c678967 100644 --- a/aclocal.m4 +++ b/aclocal.m4 @@ -1332,6 +1332,7 @@ m4_include([m4/realloc.m4]) m4_include([m4/regex.m4]) m4_include([m4/remove.m4]) m4_include([m4/rename.m4]) +m4_include([m4/renameat.m4]) m4_include([m4/rewinddir.m4]) m4_include([m4/rmdir.m4]) m4_include([m4/rpmatch.m4]) diff --git a/bootstrap.conf b/bootstrap.conf index 7def1f9..9b7c913 100644 --- a/bootstrap.conf +++ b/bootstrap.conf @@ -199,6 +199,8 @@ gnulib_modules=" regex remove rename + renameat + renameat2 rmdir root-dev-ino rpmatch diff --git a/gnulib-tests/gnulib.mk b/gnulib-tests/gnulib.mk index b2da030..38d439c 100644 --- a/gnulib-tests/gnulib.mk +++ b/gnulib-tests/gnulib.mk @@ -1676,6 +1676,24 @@ EXTRA_DIST += test-rename.h test-rename.c signature.h macros.h ## end gnulib module rename-tests +## begin gnulib module renameat-tests + +TESTS += test-renameat +check_PROGRAMS += test-renameat +test_renameat_LDADD = $(LDADD) @LIBINTL@ +EXTRA_DIST += test-rename.h test-renameat.c signature.h macros.h + +## end gnulib module renameat-tests + +## begin gnulib module renameat2-tests + +TESTS += test-renameat2 +check_PROGRAMS += test-renameat2 +test_renameat2_LDADD = $(LDADD) @LIBINTL@ +EXTRA_DIST += test-rename.h test-renameat2.c signature.h macros.h + +## end gnulib module renameat2-tests + ## begin gnulib module rmdir-tests TESTS += test-rmdir diff --git a/gnulib-tests/test-renameat.c b/gnulib-tests/test-renameat.c new file mode 100644 index 0000000..ac96d88 --- /dev/null +++ b/gnulib-tests/test-renameat.c @@ -0,0 +1,206 @@ +/* Tests of renameat. + Copyright (C) 2009-2017 Free Software Foundation, Inc. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . */ + +/* Written by Eric Blake , 2009. */ + +#include + +#include + +#include "signature.h" +SIGNATURE_CHECK (renameat, int, (int, char const *, int, char const *)); + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "filenamecat.h" +#include "ignore-value.h" +#include "macros.h" + +#define BASE "test-renameat.t" + +#include "test-rename.h" + +static int dfd1 = AT_FDCWD; +static int dfd2 = AT_FDCWD; + +/* Wrapper to test renameat like rename. */ +static int +do_rename (char const *name1, char const *name2) +{ + return renameat (dfd1, name1, dfd2, name2); +} + +int +main (void) +{ + int i; + int dfd; + char *cwd; + int result; + + /* Clean up any trash from prior testsuite runs. */ + ignore_value (system ("rm -rf " BASE "*")); + + /* Test behaviour for invalid file descriptors. */ + { + errno = 0; + ASSERT (renameat (-1, "foo", AT_FDCWD, "bar") == -1); + ASSERT (errno == EBADF); + } + { + close (99); + errno = 0; + ASSERT (renameat (99, "foo", AT_FDCWD, "bar") == -1); + ASSERT (errno == EBADF); + } + ASSERT (close (creat (BASE "oo", 0600)) == 0); + { + errno = 0; + ASSERT (renameat (AT_FDCWD, BASE "oo", -1, "bar") == -1); + ASSERT (errno == EBADF); + } + { + errno = 0; + ASSERT (renameat (AT_FDCWD, BASE "oo", 99, "bar") == -1); + ASSERT (errno == EBADF); + } + ASSERT (unlink (BASE "oo") == 0); + + /* Test basic rename functionality, using current directory. */ + result = test_rename (do_rename, false); + dfd1 = open (".", O_RDONLY); + ASSERT (0 <= dfd1); + ASSERT (test_rename (do_rename, false) == result); + dfd2 = dfd1; + ASSERT (test_rename (do_rename, false) == result); + dfd1 = AT_FDCWD; + ASSERT (test_rename (do_rename, false) == result); + ASSERT (close (dfd2) == 0); + + /* Create locations to manipulate. */ + ASSERT (mkdir (BASE "sub1", 0700) == 0); + ASSERT (mkdir (BASE "sub2", 0700) == 0); + dfd = creat (BASE "00", 0600); + ASSERT (0 <= dfd); + ASSERT (close (dfd) == 0); + cwd = getcwd (NULL, 0); + ASSERT (cwd); + + dfd = open (BASE "sub1", O_RDONLY); + ASSERT (0 <= dfd); + ASSERT (chdir (BASE "sub2") == 0); + + /* There are 16 possible scenarios, based on whether an fd is + AT_FDCWD or real, and whether a file is absolute or relative. + + To ensure that we test all of the code paths (rather than + triggering early normalization optimizations), we use a loop to + repeatedly rename a file in the parent directory, use an fd open + on subdirectory 1, all while executing in subdirectory 2; all + relative names are thus given with a leading "../". Finally, the + last scenario (two relative paths given, neither one AT_FDCWD) + has two paths, based on whether the two fds are equivalent, so we + do the other variant after the loop. */ + for (i = 0; i < 16; i++) + { + int fd1 = (i & 8) ? dfd : AT_FDCWD; + char *file1 = file_name_concat ((i & 4) ? ".." : cwd, BASE "xx", NULL); + int fd2 = (i & 2) ? dfd : AT_FDCWD; + char *file2 = file_name_concat ((i & 1) ? ".." : cwd, BASE "xx", NULL); + + ASSERT (sprintf (strchr (file1, '\0') - 2, "%02d", i) == 2); + ASSERT (sprintf (strchr (file2, '\0') - 2, "%02d", i + 1) == 2); + ASSERT (renameat (fd1, file1, fd2, file2) == 0); + free (file1); + free (file2); + } + dfd2 = open ("..", O_RDONLY); + ASSERT (0 <= dfd2); + ASSERT (renameat (dfd, "../" BASE "16", dfd2, BASE "17") == 0); + ASSERT (close (dfd2) == 0); + + /* Now we change back to the parent directory, and set dfd to "."; + using dfd in remaining tests will expose any bugs if emulation + via /proc/self/fd doesn't check for empty names. */ + ASSERT (chdir ("..") == 0); + ASSERT (close (dfd) == 0); + dfd = open (".", O_RDONLY); + ASSERT (0 <= dfd); + + ASSERT (close (creat (BASE "sub2/file", 0600)) == 0); + errno = 0; + ASSERT (renameat (dfd, BASE "sub1", dfd, BASE "sub2") == -1); + ASSERT (errno == EEXIST || errno == ENOTEMPTY); + ASSERT (unlink (BASE "sub2/file") == 0); + errno = 0; + ASSERT (renameat (dfd, BASE "sub2", dfd, BASE "sub1/.") == -1); + ASSERT (errno == EINVAL || errno == EISDIR || errno == EBUSY + || errno == ENOTEMPTY || errno == EEXIST); + errno = 0; + ASSERT (renameat (dfd, BASE "sub2/.", dfd, BASE "sub1") == -1); + ASSERT (errno == EINVAL || errno == EBUSY || errno == EEXIST); + errno = 0; + ASSERT (renameat (dfd, BASE "17", dfd, BASE "sub1") == -1); + ASSERT (errno == EISDIR); + errno = 0; + ASSERT (renameat (dfd, BASE "nosuch", dfd, BASE "18") == -1); + ASSERT (errno == ENOENT); + errno = 0; + ASSERT (renameat (dfd, "", dfd, BASE "17") == -1); + ASSERT (errno == ENOENT); + errno = 0; + ASSERT (renameat (dfd, BASE "17", dfd, "") == -1); + ASSERT (errno == ENOENT); + errno = 0; + ASSERT (renameat (dfd, BASE "sub2", dfd, BASE "17") == -1); + ASSERT (errno == ENOTDIR); + errno = 0; + ASSERT (renameat (dfd, BASE "17/", dfd, BASE "18") == -1); + ASSERT (errno == ENOTDIR); + errno = 0; + ASSERT (renameat (dfd, BASE "17", dfd, BASE "18/") == -1); + ASSERT (errno == ENOTDIR || errno == ENOENT); + + /* Finally, make sure we can overwrite existing files. */ + ASSERT (close (creat (BASE "sub2/file", 0600)) == 0); + errno = 0; + ASSERT (renameat (dfd, BASE "sub2", dfd, BASE "sub1") == 0); + ASSERT (renameat (dfd, BASE "sub1/file", dfd, BASE "17") == 0); + + /* Cleanup. */ + ASSERT (close (dfd) == 0); + errno = 0; + ASSERT (unlink (BASE "sub1/file") == -1); + ASSERT (errno == ENOENT); + ASSERT (unlink (BASE "17") == 0); + ASSERT (rmdir (BASE "sub1") == 0); + errno = 0; + ASSERT (rmdir (BASE "sub2") == -1); + ASSERT (errno == ENOENT); + free (cwd); + + if (result) + fputs ("skipping test: symlinks not supported on this file system\n", + stderr); + return result; +} diff --git a/gnulib-tests/test-renameat2.c b/gnulib-tests/test-renameat2.c new file mode 100644 index 0000000..7c250ea --- /dev/null +++ b/gnulib-tests/test-renameat2.c @@ -0,0 +1,209 @@ +/* Test renameat2. + Copyright (C) 2009-2017 Free Software Foundation, Inc. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . */ + +/* Written by Eric Blake , 2009. */ + +#include + +#include + +#include + +#include "signature.h" +SIGNATURE_CHECK (renameat2, int, + (int, char const *, int, char const *, unsigned int)); + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "filenamecat.h" +#include "ignore-value.h" +#include "macros.h" + +#define BASE "test-renameat2.t" + +#include "test-rename.h" + +static int dfd1 = AT_FDCWD; +static int dfd2 = AT_FDCWD; + +/* Wrapper to test renameat2 like rename. */ +static int +do_rename (char const *name1, char const *name2) +{ + return renameat2 (dfd1, name1, dfd2, name2, 0); +} + +int +main (void) +{ + int i; + int dfd; + char *cwd; + int result; + + /* Clean up any trash from prior testsuite runs. */ + ignore_value (system ("rm -rf " BASE "*")); + + /* Test behaviour for invalid file descriptors. */ + { + errno = 0; + ASSERT (renameat2 (-1, "foo", AT_FDCWD, "bar", 0) == -1); + ASSERT (errno == EBADF); + } + { + close (99); + errno = 0; + ASSERT (renameat2 (99, "foo", AT_FDCWD, "bar", 0) == -1); + ASSERT (errno == EBADF); + } + ASSERT (close (creat (BASE "oo", 0600)) == 0); + { + errno = 0; + ASSERT (renameat2 (AT_FDCWD, BASE "oo", -1, "bar", 0) == -1); + ASSERT (errno == EBADF); + } + { + errno = 0; + ASSERT (renameat2 (AT_FDCWD, BASE "oo", 99, "bar", 0) == -1); + ASSERT (errno == EBADF); + } + ASSERT (unlink (BASE "oo") == 0); + + /* Test basic rename functionality, using current directory. */ + result = test_rename (do_rename, false); + dfd1 = open (".", O_RDONLY); + ASSERT (0 <= dfd1); + ASSERT (test_rename (do_rename, false) == result); + dfd2 = dfd1; + ASSERT (test_rename (do_rename, false) == result); + dfd1 = AT_FDCWD; + ASSERT (test_rename (do_rename, false) == result); + ASSERT (close (dfd2) == 0); + + /* Create locations to manipulate. */ + ASSERT (mkdir (BASE "sub1", 0700) == 0); + ASSERT (mkdir (BASE "sub2", 0700) == 0); + dfd = creat (BASE "00", 0600); + ASSERT (0 <= dfd); + ASSERT (close (dfd) == 0); + cwd = getcwd (NULL, 0); + ASSERT (cwd); + + dfd = open (BASE "sub1", O_RDONLY); + ASSERT (0 <= dfd); + ASSERT (chdir (BASE "sub2") == 0); + + /* There are 16 possible scenarios, based on whether an fd is + AT_FDCWD or real, and whether a file is absolute or relative. + + To ensure that we test all of the code paths (rather than + triggering early normalization optimizations), we use a loop to + repeatedly rename a file in the parent directory, use an fd open + on subdirectory 1, all while executing in subdirectory 2; all + relative names are thus given with a leading "../". Finally, the + last scenario (two relative paths given, neither one AT_FDCWD) + has two paths, based on whether the two fds are equivalent, so we + do the other variant after the loop. */ + for (i = 0; i < 16; i++) + { + int fd1 = (i & 8) ? dfd : AT_FDCWD; + char *file1 = file_name_concat ((i & 4) ? ".." : cwd, BASE "xx", NULL); + int fd2 = (i & 2) ? dfd : AT_FDCWD; + char *file2 = file_name_concat ((i & 1) ? ".." : cwd, BASE "xx", NULL); + + ASSERT (sprintf (strchr (file1, '\0') - 2, "%02d", i) == 2); + ASSERT (sprintf (strchr (file2, '\0') - 2, "%02d", i + 1) == 2); + ASSERT (renameat2 (fd1, file1, fd2, file2, 0) == 0); + free (file1); + free (file2); + } + dfd2 = open ("..", O_RDONLY); + ASSERT (0 <= dfd2); + ASSERT (renameat2 (dfd, "../" BASE "16", dfd2, BASE "17", 0) == 0); + ASSERT (close (dfd2) == 0); + + /* Now we change back to the parent directory, and set dfd to "."; + using dfd in remaining tests will expose any bugs if emulation + via /proc/self/fd doesn't check for empty names. */ + ASSERT (chdir ("..") == 0); + ASSERT (close (dfd) == 0); + dfd = open (".", O_RDONLY); + ASSERT (0 <= dfd); + + ASSERT (close (creat (BASE "sub2/file", 0600)) == 0); + errno = 0; + ASSERT (renameat2 (dfd, BASE "sub1", dfd, BASE "sub2", 0) == -1); + ASSERT (errno == EEXIST || errno == ENOTEMPTY); + ASSERT (unlink (BASE "sub2/file") == 0); + errno = 0; + ASSERT (renameat2 (dfd, BASE "sub2", dfd, BASE "sub1/.", 0) == -1); + ASSERT (errno == EINVAL || errno == EISDIR || errno == EBUSY + || errno == ENOTEMPTY || errno == EEXIST); + errno = 0; + ASSERT (renameat2 (dfd, BASE "sub2/.", dfd, BASE "sub1", 0) == -1); + ASSERT (errno == EINVAL || errno == EBUSY || errno == EEXIST); + errno = 0; + ASSERT (renameat2 (dfd, BASE "17", dfd, BASE "sub1", 0) == -1); + ASSERT (errno == EISDIR); + errno = 0; + ASSERT (renameat2 (dfd, BASE "nosuch", dfd, BASE "18", 0) == -1); + ASSERT (errno == ENOENT); + errno = 0; + ASSERT (renameat2 (dfd, "", dfd, BASE "17", 0) == -1); + ASSERT (errno == ENOENT); + errno = 0; + ASSERT (renameat2 (dfd, BASE "17", dfd, "", 0) == -1); + ASSERT (errno == ENOENT); + errno = 0; + ASSERT (renameat2 (dfd, BASE "sub2", dfd, BASE "17", 0) == -1); + ASSERT (errno == ENOTDIR); + errno = 0; + ASSERT (renameat2 (dfd, BASE "17/", dfd, BASE "18", 0) == -1); + ASSERT (errno == ENOTDIR); + errno = 0; + ASSERT (renameat2 (dfd, BASE "17", dfd, BASE "18/", 0) == -1); + ASSERT (errno == ENOTDIR || errno == ENOENT); + + /* Finally, make sure we cannot overwrite existing files. */ + ASSERT (close (creat (BASE "sub2/file", 0600)) == 0); + errno = 0; + ASSERT ((renameat2 (dfd, BASE "sub2", dfd, BASE "sub1", RENAME_NOREPLACE) + == -1) + && errno == EEXIST); + ASSERT ((renameat2 (dfd, BASE "sub2/file", dfd, BASE "17", RENAME_NOREPLACE) + == -1) + && errno == EEXIST); + + /* Cleanup. */ + ASSERT (close (dfd) == 0); + ASSERT (unlink (BASE "sub2/file") == 0); + ASSERT (unlink (BASE "17") == 0); + ASSERT (rmdir (BASE "sub1") == 0); + ASSERT (rmdir (BASE "sub2") == 0); + free (cwd); + + if (result) + fputs ("skipping test: symlinks not supported on this file system\n", + stderr); + return result; +} diff --git a/lib/gnulib.mk b/lib/gnulib.mk index 844791b..76729b0 100644 --- a/lib/gnulib.mk +++ b/lib/gnulib.mk @@ -21,7 +21,7 @@ # the same distribution terms as the rest of that program. # # Generated by gnulib-tool. -# Reproduce by: gnulib-tool --import --dir=. --local-dir=gl --lib=libcoreutils --source-base=lib --m4-base=m4 --doc-base=doc --tests-base=gnulib-tests --aux-dir=build-aux --with-tests --avoid=canonicalize-lgpl --avoid=dummy --makefile-name=gnulib.mk --no-conditional-dependencies --no-libtool --macro-prefix=gl acl alignof alloca announce-gen areadlink-with-size argmatch argv-iter assert autobuild backupfile base64 buffer-lcm c-strcase c-strtod c-strtold calloc-gnu canon-host canonicalize chown cloexec closein closeout config-h configmake crypto/md5 crypto/sha1 crypto/sha256 crypto/sha512 cycle-check d-ino d-type di-set diacrit dirfd dirname do-release-commit-and-tag dtoastr dup2 environ error euidaccess exclude exitfail faccessat fadvise fchdir fchmodat fchownat fclose fcntl fcntl-safer fd-reopen fdatasync fdl fdopen fdutimensat file-type fileblocks filemode filenamecat filevercmp fnmatch-gnu fopen-safer fprintftime freopen freopen-safer fseeko fstatat fsusage fsync ftello ftoastr ftruncate fts full-read full-write getgroups gethrxtime getline getloadavg getlogin getndelim2 getopt-gnu getpagesize getpass-gnu gettext-h gettime gettimeofday getugroups getusershell git-version-gen gitlog-to-changelog gnu-make gnu-web-doc-update gnumakefile gnupload group-member hard-locale hash hash-pjw heap host-os human idcache ignore-value inttostr inttypes isapipe isatty isblank largefile lchmod lchown ldtoastr lib-ignore linebuffer link link-follow linkat long-options lstat maintainer-makefile malloc-gnu manywarnings mbrlen mbrtowc mbsalign mbswidth memcasecmp memchr memcmp2 mempcpy memrchr mgetgroups mkancesdirs mkdir mkdir-p mkfifo mknod mkstemp mktime modechange mountlist mpsort netinet_in non-recursive-gnulib-prefix-hack nproc obstack parse-datetime pathmax perl physmem pipe posix-shell posixtm posixver priv-set progname propername pthread putenv quote quotearg randint randperm read-file readlink readtokens readtokens0 readutmp realloc-gnu regex remove rename rmdir root-dev-ino rpmatch safe-read same save-cwd savedir savewd selinux-at settime sig2str sigaction smack ssize_t stat-macros stat-size stat-time statat stdbool stdlib-safer stpcpy stpncpy strdup-posix strftime strncat strnumcmp strpbrk strsignal strtod strtoimax strtoumax symlink sys_ioctl sys_resource sys_stat sys_wait termios timer-time timespec tzset uname unicodeio unistd-safer unlink-busy unlinkat unlocked-io unsetenv update-copyright uptime useless-if-before-free userspec utimecmp utimens vasprintf-posix vc-list-files verify verror version-etc-fsf wcswidth wcwidth winsz-ioctl winsz-termios write-any-file xalloc xfreopen xfts xgetcwd xgetgroups xgethostname xmemcoll xnanosleep xprintf xprintf-posix xreadlink xstrtod xstrtoimax xstrtol xstrtold xstrtoumax yesno +# Reproduce by: gnulib-tool --import --dir=. --local-dir=gl --lib=libcoreutils --source-base=lib --m4-base=m4 --doc-base=doc --tests-base=gnulib-tests --aux-dir=build-aux --with-tests --avoid=canonicalize-lgpl --avoid=dummy --makefile-name=gnulib.mk --no-conditional-dependencies --no-libtool --macro-prefix=gl acl alignof alloca announce-gen areadlink-with-size argmatch argv-iter assert autobuild backupfile base64 buffer-lcm c-strcase c-strtod c-strtold calloc-gnu canon-host canonicalize chown cloexec closein closeout config-h configmake crypto/md5 crypto/sha1 crypto/sha256 crypto/sha512 cycle-check d-ino d-type di-set diacrit dirfd dirname do-release-commit-and-tag dtoastr dup2 environ error euidaccess exclude exitfail faccessat fadvise fchdir fchmodat fchownat fclose fcntl fcntl-safer fd-reopen fdatasync fdl fdopen fdutimensat file-type fileblocks filemode filenamecat filevercmp fnmatch-gnu fopen-safer fprintftime freopen freopen-safer fseeko fstatat fsusage fsync ftello ftoastr ftruncate fts full-read full-write getgroups gethrxtime getline getloadavg getlogin getndelim2 getopt-gnu getpagesize getpass-gnu gettext-h gettime gettimeofday getugroups getusershell git-version-gen gitlog-to-changelog gnu-make gnu-web-doc-update gnumakefile gnupload group-member hard-locale hash hash-pjw heap host-os human idcache ignore-value inttostr inttypes isapipe isatty isblank largefile lchmod lchown ldtoastr lib-ignore linebuffer link link-follow linkat long-options lstat maintainer-makefile malloc-gnu manywarnings mbrlen mbrtowc mbsalign mbswidth memcasecmp memchr memcmp2 mempcpy memrchr mgetgroups mkancesdirs mkdir mkdir-p mkfifo mknod mkstemp mktime modechange mountlist mpsort netinet_in non-recursive-gnulib-prefix-hack nproc obstack parse-datetime pathmax perl physmem pipe posix-shell posixtm posixver priv-set progname propername pthread putenv quote quotearg randint randperm read-file readlink readtokens readtokens0 readutmp realloc-gnu regex remove rename renameat renameat2 rmdir root-dev-ino rpmatch safe-read same save-cwd savedir savewd selinux-at settime sig2str sigaction smack ssize_t stat-macros stat-size stat-time statat stdbool stdlib-safer stpcpy stpncpy strdup-posix strftime strncat strnumcmp strpbrk strsignal strtod strtoimax strtoumax symlink sys_ioctl sys_resource sys_stat sys_wait termios timer-time timespec tzset uname unicodeio unistd-safer unlink-busy unlinkat unlocked-io unsetenv update-copyright uptime useless-if-before-free userspec utimecmp utimens vasprintf-posix vc-list-files verify verror version-etc-fsf wcswidth wcwidth winsz-ioctl winsz-termios write-any-file xalloc xfreopen xfts xgetcwd xgetgroups xgethostname xmemcoll xnanosleep xprintf xprintf-posix xreadlink xstrtod xstrtoimax xstrtol xstrtold xstrtoumax yesno MOSTLYCLEANFILES += lib/core lib/*.stackdump @@ -3202,6 +3202,25 @@ EXTRA_lib_libcoreutils_a_SOURCES += lib/rename.c ## end gnulib module rename +## begin gnulib module renameat + + +EXTRA_DIST += lib/renameat.c + +EXTRA_lib_libcoreutils_a_SOURCES += lib/renameat.c + +## end gnulib module renameat + +## begin gnulib module renameat2 + +lib_libcoreutils_a_SOURCES += lib/renameat2.c + +EXTRA_DIST += lib/at-func2.c lib/renameat2.h + +EXTRA_lib_libcoreutils_a_SOURCES += lib/at-func2.c + +## end gnulib module renameat2 + ## begin gnulib module rewinddir diff --git a/lib/renameat.c b/lib/renameat.c new file mode 100644 index 0000000..48cee4b --- /dev/null +++ b/lib/renameat.c @@ -0,0 +1,25 @@ +/* Rename a file relative to open directories. + Copyright 2017 Free Software Foundation, Inc. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . */ + +#include +#include +#include "renameat2.h" + +int +renameat (int fd1, char const *src, int fd2, char const *dst) +{ + return renameat2 (fd1, src, fd2, dst, 0); +} diff --git a/lib/renameat2.c b/lib/renameat2.c new file mode 100644 index 0000000..26cde86 --- /dev/null +++ b/lib/renameat2.c @@ -0,0 +1,227 @@ +/* Rename a file relative to open directories. + Copyright (C) 2009-2017 Free Software Foundation, Inc. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . */ + +/* written by Eric Blake and Paul Eggert */ + +#include + +#include "renameat2.h" + +#include +#include +#include +#include + +#ifdef __linux__ +# include +#endif + +static int +errno_fail (int e) +{ + errno = e; + return -1; +} + +#if HAVE_RENAMEAT + +# include +# include +# include + +# include "dirname.h" +# include "openat.h" + +#else +# include "openat-priv.h" + +static int +rename_noreplace (char const *src, char const *dst) +{ + /* This has a race between the call to lstat and the call to rename. */ + struct stat st; + return (lstat (dst, &st) == 0 || errno == EOVERFLOW ? errno_fail (EEXIST) + : errno == ENOENT ? rename (src, dst) + : -1); +} +#endif + +#undef renameat + +/* Rename FILE1, in the directory open on descriptor FD1, to FILE2, in + the directory open on descriptor FD2. If possible, do it without + changing the working directory. Otherwise, resort to using + save_cwd/fchdir, then rename/restore_cwd. If either the save_cwd or + the restore_cwd fails, then give a diagnostic and exit nonzero. + + Obey FLAGS when doing the renaming. If FLAGS is zero, this + function is equivalent to renameat (FD1, SRC, FD2, DST). */ + +int +renameat2 (int fd1, char const *src, int fd2, char const *dst, + unsigned int flags) +{ + int ret_val = -1; + int err = EINVAL; + +#ifdef SYS_renameat2 + ret_val = syscall (SYS_renameat2, fd1, src, fd2, dst, flags); + err = errno; +#elif defined RENAME_EXCL + if (! (flags & ~(RENAME_EXCHANGE | RENAME_NOREPLACE))) + { + ret_val = renameatx_np (fd1, src, fd2, dst, + ((flags & RENAME_EXCHANGE ? RENAME_SWAP : 0) + | (flags & RENAME_NOREPLACE ? RENAME_EXCL : 0))); + err = errno; + } +#endif + + if (! (ret_val < 0 && (err == EINVAL || err == ENOSYS || err == ENOTSUP))) + return ret_val; + +#if HAVE_RENAMEAT + { + size_t src_len; + size_t dst_len; + char *src_temp = (char *) src; + char *dst_temp = (char *) dst; + bool src_slash; + bool dst_slash; + int rename_errno = ENOTDIR; + struct stat src_st; + struct stat dst_st; + bool dst_found_nonexistent = false; + + if (flags != 0) + { + /* RENAME_NOREPLACE is the only flag currently supported. */ + if (flags & ~RENAME_NOREPLACE) + return errno_fail (ENOTSUP); + else + { + /* This has a race between the call to lstatat and the calls to + renameat below. */ + if (lstatat (fd2, dst, &dst_st) == 0 || errno == EOVERFLOW) + return errno_fail (EEXIST); + if (errno != ENOENT) + return -1; + dst_found_nonexistent = true; + } + } + + /* Let strace see any ENOENT failure. */ + src_len = strlen (src); + dst_len = strlen (dst); + if (!src_len || !dst_len) + return renameat (fd1, src, fd2, dst); + + src_slash = src[src_len - 1] == '/'; + dst_slash = dst[dst_len - 1] == '/'; + if (!src_slash && !dst_slash) + return renameat (fd1, src, fd2, dst); + + /* Presence of a trailing slash requires directory semantics. If + the source does not exist, or if the destination cannot be turned + into a directory, give up now. Otherwise, strip trailing slashes + before calling rename. */ + if (lstatat (fd1, src, &src_st)) + return -1; + if (dst_found_nonexistent) + { + if (!S_ISDIR (src_st.st_mode)) + return errno_fail (ENOENT); + } + else if (lstatat (fd2, dst, &dst_st)) + { + if (errno != ENOENT || !S_ISDIR (src_st.st_mode)) + return -1; + } + else if (!S_ISDIR (dst_st.st_mode)) + return errno_fail (ENOTDIR); + else if (!S_ISDIR (src_st.st_mode)) + return errno_fail (EISDIR); + +# if RENAME_TRAILING_SLASH_SOURCE_BUG + /* See the lengthy comment in rename.c why Solaris 9 is forced to + GNU behavior, while Solaris 10 is left with POSIX behavior, + regarding symlinks with trailing slash. */ + ret_val = -1; + if (src_slash) + { + src_temp = strdup (src); + if (!src_temp) + { + /* Rather than rely on strdup-posix, we set errno ourselves. */ + rename_errno = ENOMEM; + goto out; + } + strip_trailing_slashes (src_temp); + if (lstatat (fd1, src_temp, &src_st)) + { + rename_errno = errno; + goto out; + } + if (S_ISLNK (src_st.st_mode)) + goto out; + } + if (dst_slash) + { + dst_temp = strdup (dst); + if (!dst_temp) + { + rename_errno = ENOMEM; + goto out; + } + strip_trailing_slashes (dst_temp); + if (lstatat (fd2, dst_temp, &dst_st)) + { + if (errno != ENOENT) + { + rename_errno = errno; + goto out; + } + } + else if (S_ISLNK (dst_st.st_mode)) + goto out; + } +# endif /* RENAME_TRAILING_SLASH_SOURCE_BUG */ + + /* renameat does not honor trailing / on Solaris 10. Solve it in a + similar manner to rename. No need to worry about bugs not present + on Solaris, since all other systems either lack renameat or honor + trailing slash correctly. */ + + ret_val = renameat (fd1, src_temp, fd2, dst_temp); + rename_errno = errno; + goto out; + out: + if (src_temp != src) + free (src_temp); + if (dst_temp != dst) + free (dst_temp); + errno = rename_errno; + return ret_val; + } +#else /* !HAVE_RENAMEAT */ + + /* RENAME_NOREPLACE is the only flag currently supported. */ + if (flags & ~RENAME_NOREPLACE) + return errno_fail (ENOTSUP); + return at_func2 (fd1, src, fd2, dst, flags ? rename_noreplace : rename); + +#endif /* !HAVE_RENAMEAT */ +} diff --git a/lib/renameat2.h b/lib/renameat2.h new file mode 100644 index 0000000..179210f --- /dev/null +++ b/lib/renameat2.h @@ -0,0 +1,30 @@ +/* Rename a file relative to open directories. + Copyright 2017 Free Software Foundation, Inc. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . */ + +/* written by Paul Eggert */ + +/* Get RENAME_* macros from linux/fs.h if present, otherwise supply + the traditional Linux values. */ +#if HAVE_LINUX_FS_H +# include +#endif +#ifndef RENAME_NOREPLACE +# define RENAME_NOREPLACE (1 << 0) +# define RENAME_EXCHANGE (1 << 1) +# define RENAME_WHITEOUT (1 << 2) +#endif + +extern int renameat2 (int, char const *, int, char const *, unsigned int); diff --git a/m4/gnulib-comp.m4 b/m4/gnulib-comp.m4 index 4ef3c43..309e308 100644 --- a/m4/gnulib-comp.m4 +++ b/m4/gnulib-comp.m4 @@ -547,6 +547,10 @@ AC_DEFUN([gl_EARLY], # Code from module remove-tests: # Code from module rename: # Code from module rename-tests: + # Code from module renameat: + # Code from module renameat-tests: + # Code from module renameat2: + # Code from module renameat2-tests: # Code from module rewinddir: # Code from module rmdir: # Code from module rmdir-tests: @@ -1696,6 +1700,18 @@ AC_DEFUN([gl_INIT], AC_LIBOBJ([rename]) fi gl_STDIO_MODULE_INDICATOR([rename]) + gl_FUNC_RENAMEAT + if test $HAVE_RENAMEAT = 0 || test $REPLACE_RENAMEAT = 1; then + AC_LIBOBJ([renameat]) + fi + if test $HAVE_RENAMEAT = 0; then + AC_LIBOBJ([at-func2]) + fi + gl_STDIO_MODULE_INDICATOR([renameat]) + gl_FUNC_RENAMEAT + if test $HAVE_RENAMEAT = 0; then + AC_LIBOBJ([at-func2]) + fi gl_FUNC_REWINDDIR if test $HAVE_REWINDDIR = 0; then AC_LIBOBJ([rewinddir]) @@ -2868,6 +2884,9 @@ AC_DEFUN([gl_FILE_LIST], [ lib/regexec.c lib/remove.c lib/rename.c + lib/renameat.c + lib/renameat2.c + lib/renameat2.h lib/rewinddir.c lib/rmdir.c lib/root-dev-ino.c @@ -3372,6 +3391,7 @@ AC_DEFUN([gl_FILE_LIST], [ m4/regex.m4 m4/remove.m4 m4/rename.m4 + m4/renameat.m4 m4/rewinddir.m4 m4/rmdir.m4 m4/rpmatch.m4 @@ -3794,6 +3814,8 @@ AC_DEFUN([gl_FILE_LIST], [ tests/test-remove.c tests/test-rename.c tests/test-rename.h + tests/test-renameat.c + tests/test-renameat2.c tests/test-rmdir.c tests/test-rmdir.h tests/test-sameacls.c diff --git a/m4/renameat.m4 b/m4/renameat.m4 new file mode 100644 index 0000000..1b97774 --- /dev/null +++ b/m4/renameat.m4 @@ -0,0 +1,25 @@ +# serial 3 +# See if we need to provide renameat replacement. + +dnl Copyright (C) 2009-2017 Free Software Foundation, Inc. +dnl This file is free software; the Free Software Foundation +dnl gives unlimited permission to copy and/or distribute it, +dnl with or without modifications, as long as this notice is preserved. + +# Written by Eric Blake. + +AC_DEFUN([gl_FUNC_RENAMEAT], +[ + AC_REQUIRE([gl_FUNC_OPENAT]) + AC_REQUIRE([gl_FUNC_RENAME]) + AC_REQUIRE([gl_STDIO_H_DEFAULTS]) + AC_REQUIRE([gl_USE_SYSTEM_EXTENSIONS]) + AC_CHECK_HEADERS([linux/fs.h]) + AC_CHECK_FUNCS_ONCE([renameat]) + if test $ac_cv_func_renameat = no; then + HAVE_RENAMEAT=0 + elif test $REPLACE_RENAME = 1; then + dnl Solaris 9 and 10 have the same bugs in renameat as in rename. + REPLACE_RENAMEAT=1 + fi +]) diff --git a/src/copy.c b/src/copy.c index 2a804945e..be4e357a8 100644 --- a/src/copy.c +++ b/src/copy.c @@ -51,6 +51,7 @@ #include "ignore-value.h" #include "ioblksize.h" #include "quote.h" +#include "renameat2.h" #include "root-uid.h" #include "same.h" #include "savedir.h" @@ -2093,8 +2094,9 @@ copy_internal (char const *src_name, char const *dst_name, /* If the source is a directory, we don't always create the destination directory. So --verbose should not announce anything until we're - sure we'll create a directory. */ - if (x->verbose && !S_ISDIR (src_mode)) + sure we'll create a directory. In move mode we delay the diagnostic + message until we know whether renameat2() has actually succeeded. */ + if (x->verbose && !S_ISDIR (src_mode) && !x->move_mode) emit_verbose (src_name, dst_name, backup_succeeded ? dst_backup : NULL); /* Associate the destination file name with the source device and inode @@ -2196,9 +2198,14 @@ copy_internal (char const *src_name, char const *dst_name, if (x->move_mode) { - if (rename (src_name, dst_name) == 0) + int flags = 0; + if (x->interactive == I_ALWAYS_NO) + /* do not replace DST_NAME if it was created since our last check */ + flags = RENAME_NOREPLACE; + + if (renameat2 (AT_FDCWD, src_name, AT_FDCWD, dst_name, flags) == 0) { - if (x->verbose && S_ISDIR (src_mode)) + if (x->verbose) emit_verbose (src_name, dst_name, backup_succeeded ? dst_backup : NULL); @@ -2226,6 +2233,15 @@ copy_internal (char const *src_name, char const *dst_name, return true; } + if ((flags & RENAME_NOREPLACE) && (errno == EEXIST)) + { + /* Pretend the rename succeeded, so the caller (mv) + doesn't end up removing the source file. */ + if (rename_succeeded) + *rename_succeeded = true; + return true; + } + /* FIXME: someday, consider what to do when moving a directory into itself but when source and destination are on different devices. */ @@ -2301,6 +2317,9 @@ copy_internal (char const *src_name, char const *dst_name, return false; } + if (x->verbose && !S_ISDIR (src_mode)) + emit_verbose (src_name, dst_name, backup_succeeded ? dst_backup : NULL); + new_dst = true; } -- 2.13.6