>From 376967889ed7ed561e46ff6d88a66779db62737a Mon Sep 17 00:00:00 2001 From: Paul Eggert
Date: Sat, 11 Feb 2017 23:12:31 -0800 Subject: [PATCH] ln: replace destination links more atomically If the file B already exists, commands like 'ln -f A B' and 'cp -fl A B' no longer remove B before creating the new link. Instead, they arrange for the new link to replace B atomically. This should fix a race condition reported by Mike Crowe (Bug#25680). * NEWS, doc/coreutils.texi (cp invocation, ln invocation): Document this. * bootstrap.conf (gnulib_modules): Add symlinkat. * src/copy.c, src/ln.c: Include force-link.h. * src/copy.c (same_file_ok): It's also OK to remove a destination symlink when creating symbolic links, or when the source and destination are on the same file system and when creating hard links. * src/copy.c (create_hard_link, copy_internal): * src/ln.c (do_link): Rewrite using force_linkat and force_symlinkat, to close a window where the destination temporarily does not exist. * src/cp.c (main): Do not set x.unlink_dest_before_opening merely because we are in link-creation mode. * src/force-link.c, src/force-link.h: New files. * src/local.mk (copy_sources, src_ln_SOURCES): Add them. * tests/cp/same-file.sh: Adjust test case to match fixed behavior. --- NEWS | 10 +++ bootstrap.conf | 2 +- doc/coreutils.texi | 18 +++-- src/copy.c | 96 +++++++++++--------------- src/cp.c | 5 -- src/force-link.c | 187 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/force-link.h | 3 + src/ln.c | 31 +++------ src/local.mk | 8 ++- tests/cp/same-file.sh | 2 +- 10 files changed, 268 insertions(+), 94 deletions(-) create mode 100644 src/force-link.c create mode 100644 src/force-link.h diff --git a/NEWS b/NEWS index deaab7b..7473e6e 100644 --- a/NEWS +++ b/NEWS @@ -2,12 +2,22 @@ GNU coreutils NEWS -*- outline -*- * Noteworthy changes in release ?.? (????-??-??) [?] +** Improvements + + If the file B already exists, commands like 'ln -f A B' and + 'cp -fl A B' no longer remove B before creating the new link. + That is, there is no longer a brief moment when B does not exist. + ** Bug fixes date again converts from a specified time zone. Previously output was not converted to the local time zone, and remained in the specified one. [bug introduced in coreutils-8.26] + Commands like 'cp --no-dereference -l A B' are no longer quiet no-ops + when A is a regular file and B is a symbolic link that points to A. + [bug introduced in fileutils-4.0] + factor no longer goes into an infinite loop for certain numbers like 158909489063877810457 and 222087527029934481871. [bug introduced in coreutils-8.20] diff --git a/bootstrap.conf b/bootstrap.conf index 59b3a91..acec6f0 100644 --- a/bootstrap.conf +++ b/bootstrap.conf @@ -234,7 +234,7 @@ gnulib_modules=" strtod strtoimax strtoumax - symlink + symlinkat sys_ioctl sys_resource sys_stat diff --git a/doc/coreutils.texi b/doc/coreutils.texi index 3eac96b..2dbfcce 100644 --- a/doc/coreutils.texi +++ b/doc/coreutils.texi @@ -8125,9 +8125,11 @@ Equivalent to @option{--no-dereference --preserve=links}. When copying without this option and an existing destination file cannot be opened for writing, the copy fails. However, with @option{--force}, when a destination file cannot be opened, @command{cp} then removes it and -tries to open it again. Contrast this behavior with that enabled by address@hidden and @option{--symbolic-link}, whereby the destination file -is never opened but rather is removed unconditionally. Also see the +tries to open it again. When this option is combined with address@hidden (@option{-l}) or @option{--symbolic-link} +(@option{-s}), the destination link is replaced, and unless address@hidden (@option{-b}) is also given there is no brief +moment when the destination does not exist. Also see the description of @option{--remove-destination}. This option is independent of the @option{--interactive} or @@ -9825,11 +9827,13 @@ directory, using the @var{target}s' names. @end itemize -Normally @command{ln} does not remove existing files. Use the address@hidden (@option{-f}) option to remove them unconditionally, -the @option{--interactive} (@option{-i}) option to remove them +Normally @command{ln} does not replace existing files. Use the address@hidden (@option{-f}) option to replace them unconditionally, +the @option{--interactive} (@option{-i}) option to replace them conditionally, and the @option{--backup} (@option{-b}) option to -rename them. +rename them. Unless the @option{--backup} (@option{-b}) option is +used there is no brief moment when the destination does not exist; +this is an extension to POSIX. @cindex hard link, defined @cindex inode, and hard links diff --git a/src/copy.c b/src/copy.c index e3832c2..9dbd536 100644 --- a/src/copy.c +++ b/src/copy.c @@ -46,6 +46,7 @@ #include "file-set.h" #include "filemode.h" #include "filenamecat.h" +#include "force-link.h" #include "full-write.h" #include "hash.h" #include "hash-triple.h" @@ -1623,11 +1624,13 @@ same_file_ok (char const *src_name, struct stat const *src_sb, } } - /* It's ok to remove a destination symlink. But that works only when we - unlink before opening the destination and when the source and destination - files are on the same partition. */ - if (x->unlink_dest_before_opening - && S_ISLNK (dst_sb_link->st_mode)) + /* It's ok to remove a destination symlink. But that works only + when creating symbolic links, or when the source and destination + are on the same file system and when creating hard links or when + unlinking before opening the destination. */ + if (x->symbolic_link + || ((x->hard_link || x->unlink_dest_before_opening) + && S_ISLNK (dst_sb_link->st_mode))) return dst_sb_link->st_dev == src_sb_link->st_dev; if (x->dereference == DEREF_NEVER) @@ -1779,36 +1782,17 @@ static bool create_hard_link (char const *src_name, char const *dst_name, bool replace, bool verbose, bool dereference) { - /* We want to guarantee that symlinks are not followed, unless requested. */ - int flags = 0; - if (dereference) - flags = AT_SYMLINK_FOLLOW; - - bool link_failed = (linkat (AT_FDCWD, src_name, AT_FDCWD, dst_name, flags) - != 0); - - /* If the link failed because of an existing destination, - remove that file and then call link again. */ - if (link_failed && replace && errno == EEXIST) - { - if (unlink (dst_name) != 0) - { - error (0, errno, _("cannot remove %s"), quoteaf (dst_name)); - return false; - } - if (verbose) - printf (_("removed %s\n"), quoteaf (dst_name)); - link_failed = (linkat (AT_FDCWD, src_name, AT_FDCWD, dst_name, flags) - != 0); - } - - if (link_failed) + int status = force_linkat (AT_FDCWD, src_name, AT_FDCWD, dst_name, + dereference ? AT_SYMLINK_FOLLOW : 0, + replace); + if (status < 0) { error (0, errno, _("cannot create hard link %s to %s"), quoteaf_n (0, dst_name), quoteaf_n (1, src_name)); return false; } - + if (0 < status && verbose) + printf (_("removed %s\n"), quoteaf (dst_name)); return true; } @@ -2592,7 +2576,9 @@ copy_internal (char const *src_name, char const *dst_name, goto un_backup; } } - if (symlink (src_name, dst_name) != 0) + if (force_symlinkat (src_name, AT_FDCWD, dst_name, + x->unlink_dest_after_failed_open) + < 0) { error (0, errno, _("cannot create symbolic link %s to %s"), quoteaf_n (0, dst_name), quoteaf_n (1, src_name)); @@ -2617,7 +2603,9 @@ copy_internal (char const *src_name, char const *dst_name, && !(! CAN_HARDLINK_SYMLINKS && S_ISLNK (src_mode) && x->dereference == DEREF_NEVER)) { - if (! create_hard_link (src_name, dst_name, false, false, dereference)) + if (! create_hard_link (src_name, dst_name, + x->unlink_dest_after_failed_open, + false, dereference)) goto un_backup; } else if (S_ISREG (src_mode) @@ -2671,33 +2659,31 @@ copy_internal (char const *src_name, char const *dst_name, goto un_backup; } - if (symlink (src_link_val, dst_name) == 0) - free (src_link_val); - else + int symlink_r = force_symlinkat (src_link_val, AT_FDCWD, dst_name, + x->unlink_dest_after_failed_open); + int symlink_err = symlink_r < 0 ? errno : 0; + if (symlink_err && x->update && !new_dst && S_ISLNK (dst_sb.st_mode) + && dst_sb.st_size == strlen (src_link_val)) { - int saved_errno = errno; - bool same_link = false; - if (x->update && !new_dst && S_ISLNK (dst_sb.st_mode) - && dst_sb.st_size == strlen (src_link_val)) + /* See if the destination is already the desired symlink. + FIXME: This behavior isn't documented, and seems wrong + in some cases, e.g., if the destination symlink has the + wrong ownership, permissions, or timestamps. */ + char *dest_link_val = + areadlink_with_size (dst_name, dst_sb.st_size); + if (dest_link_val) { - /* See if the destination is already the desired symlink. - FIXME: This behavior isn't documented, and seems wrong - in some cases, e.g., if the destination symlink has the - wrong ownership, permissions, or timestamps. */ - char *dest_link_val = - areadlink_with_size (dst_name, dst_sb.st_size); - if (dest_link_val && STREQ (dest_link_val, src_link_val)) - same_link = true; + if (STREQ (dest_link_val, src_link_val)) + symlink_err = 0; free (dest_link_val); } - free (src_link_val); - - if (! same_link) - { - error (0, saved_errno, _("cannot create symbolic link %s"), - quoteaf (dst_name)); - goto un_backup; - } + } + free (src_link_val); + if (symlink_err) + { + error (0, symlink_err, _("cannot create symbolic link %s"), + quoteaf (dst_name)); + goto un_backup; } if (x->preserve_security_context) diff --git a/src/cp.c b/src/cp.c index 0805558..88db3a3 100644 --- a/src/cp.c +++ b/src/cp.c @@ -1155,11 +1155,6 @@ main (int argc, char **argv) if (x.recursive) x.copy_as_regular = copy_contents; - /* If --force (-f) was specified and we're in link-creation mode, - first remove any existing destination file. */ - if (x.unlink_dest_after_failed_open && (x.hard_link || x.symbolic_link)) - x.unlink_dest_before_opening = true; - /* Ensure -Z overrides -a. */ if ((x.set_security_context || scontext) && ! x.require_preserve_context) diff --git a/src/force-link.c b/src/force-link.c new file mode 100644 index 0000000..e0db075 --- /dev/null +++ b/src/force-link.c @@ -0,0 +1,187 @@ +/* Implement ln -f "atomically" + + 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