On 01/04/2018 03:01 AM, Kamil Dudka wrote:
On Thursday, January 4, 2018 10:48:56 AM CET Paul Eggert wrote:
Kamil Dudka wrote:
- 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;
By then it's too late, as multiple decisions have been made on the basis of
stat/lstat calls that are still subject to races.
Do you mean in the case of mv -n? Which decisions exactly?
Mostly mv -n, but I suspect problems also for mv without -n. It's all
the decisions that depend on the result of lstat of dst_name, before
abandon_move decides whether to skip the rename. With the patch you
proposed, mv -n could call lstat and get a failure (with errno ==
ENOENT), then (after another process creates the file) call renameat2
with RENAME_NOREPLACE and after this fails (with errno == EEXIST) report
an error. mv -n should silently succeed in that case.
Sounds like a corner case. Please consider writing a separate patch
for that.
OK, that's pretty straightforward so I installed it. Please see the
first attached patch.
I had difficulties trying to evaluate the patch. It does not compile
That's what I get for sending an untested patch. Sorry. I fixed the bugs
you mentioned and tested the result. Please see the second attached
patch, which I have not installed.
There is an interesting behavior change with this second patch.
Currently, 'mv -n a a' fails with a diagnostic "mv: 'a' and 'a' are the
same file". With the patch, 'mv -n a a' silently succeeds. The coreutils
documentation allows both behaviors. I doubt whether anyone cares, and
doing it the new way avoids some syscalls so I left it that way.
>From 7e244891b0c41bbf9f5b5917d1a71c183a8367ac Mon Sep 17 00:00:00 2001
From: Paul Eggert <[email protected]>
Date: Thu, 4 Jan 2018 13:57:40 -0800
Subject: [PATCH 1/2] mv: -n overrides -u
* NEWS: Mention these fixes.
* doc/coreutils.texi (cp invocation, mv invocation):
Mention that -n is silent, and that it overrides -u.
* src/cp.c, src/mv.c (main): -n overrides -u.
---
NEWS | 7 +++++++
doc/coreutils.texi | 13 +++++++++----
src/cp.c | 3 +++
src/mv.c | 3 +++
4 files changed, 22 insertions(+), 4 deletions(-)
diff --git a/NEWS b/NEWS
index b89254f7e..4712f5a46 100644
--- a/NEWS
+++ b/NEWS
@@ -2,6 +2,13 @@ GNU coreutils NEWS -*- outline -*-
* Noteworthy changes in release ?.? (????-??-??) [?]
+** Bug fixes
+
+ 'cp -n -u' and 'mv -n -u' now consistently ignore the -u option.
+ Previously, this option combination suffered from race conditions
+ that caused -u to sometimes override -n.
+ [bug introduced with coreutils-7.1]
+
* Noteworthy changes in release 8.29 (2017-12-27) [stable]
diff --git a/doc/coreutils.texi b/doc/coreutils.texi
index 1c0e8a36c..6bb9f0906 100644
--- a/doc/coreutils.texi
+++ b/doc/coreutils.texi
@@ -8569,7 +8569,8 @@ a regular file in the destination tree.
@itemx --no-clobber
@opindex -n
@opindex --no-clobber
-Do not overwrite an existing file. The @option{-n} option overrides a previous
+Do not overwrite an existing file; silently do nothing instead.
+This option overrides a previous
@option{-i} option. This option is mutually exclusive with @option{-b} or
@option{--backup} option.
@@ -8809,8 +8810,10 @@ the comparison is to the source timestamp truncated to the
resolutions of the destination file system and of the system calls
used to update timestamps; this avoids duplicate work if several
@samp{cp -pu} commands are executed with the same source and destination.
-If @option{--preserve=links} is also specified (like with @samp{cp -au}
-for example), that will take precedence. Consequently, depending on the
+This option is ignored if the @option{-n} or @option{--no-clobber}
+option is also specified.
+Also, if @option{--preserve=links} is also specified (like with @samp{cp -au}
+for example), that will take precedence; consequently, depending on the
order that files are processed from the source, newer files in the destination
may be replaced, to mirror hard links in the source.
@@ -9657,7 +9660,7 @@ If the response is not affirmative, the file is skipped.
@opindex -n
@opindex --no-clobber
@cindex prompts, omitting
-Do not overwrite an existing file.
+Do not overwrite an existing file; silently do nothing instead.
@mvOptsIfn
This option is mutually exclusive with @option{-b} or @option{--backup} option.
@@ -9673,6 +9676,8 @@ source timestamp truncated to the resolutions of the destination file
system and of the system calls used to update timestamps; this avoids
duplicate work if several @samp{mv -u} commands are executed with the
same source and destination.
+This option is ignored if the @option{-n} or @option{--no-clobber}
+option is also specified.
@item -v
@itemx --verbose
diff --git a/src/cp.c b/src/cp.c
index 1b5bf7285..d81d41859 100644
--- a/src/cp.c
+++ b/src/cp.c
@@ -1145,6 +1145,9 @@ main (int argc, char **argv)
usage (EXIT_FAILURE);
}
+ if (x.interactive == I_ALWAYS_NO)
+ x.update = false;
+
if (make_backups && x.interactive == I_ALWAYS_NO)
{
error (0, 0,
diff --git a/src/mv.c b/src/mv.c
index a8df730a7..818ca59b6 100644
--- a/src/mv.c
+++ b/src/mv.c
@@ -459,6 +459,9 @@ main (int argc, char **argv)
quoteaf (file[n_files - 1]));
}
+ if (x.interactive == I_ALWAYS_NO)
+ x.update = false;
+
if (make_backups && x.interactive == I_ALWAYS_NO)
{
error (0, 0,
--
2.14.3
>From 09d45e7c2e9b79a33c41271f9794d4133414f313 Mon Sep 17 00:00:00 2001
From: Paul Eggert <[email protected]>
Date: Thu, 4 Jan 2018 16:46:24 -0800
Subject: [PATCH 2/2] mv: improve -n atomicity
Problem reported by Kamil Dudka (Bug#29961).
* NEWS: Mention this.
* src/copy.c: Include renameat2.h.
(copy_internal): If mv, try renameat2 first thing, with
RENAME_NOREPLACE. If this works, skip most of the remaining code.
Also, fail quickly if it fails with EEXIST, and we are using -n.
---
NEWS | 6 +++++
src/copy.c | 88 +++++++++++++++++++++++++++++++++++++++-----------------------
2 files changed, 62 insertions(+), 32 deletions(-)
diff --git a/NEWS b/NEWS
index 4712f5a46..b7ec20085 100644
--- a/NEWS
+++ b/NEWS
@@ -4,6 +4,12 @@ GNU coreutils NEWS -*- outline -*-
** Bug fixes
+ 'mv -n A B' no longer suffers from a race condition that can
+ overwrite a simultaneously-created B. This bug fix requires
+ platform support for the renameat2 or renameatx_np syscalls, found
+ in recent Linux and macOS kernels.
+ [bug introduced with coreutils-7.1]
+
'cp -n -u' and 'mv -n -u' now consistently ignore the -u option.
Previously, this option combination suffered from race conditions
that caused -u to sometimes override -n.
diff --git a/src/copy.c b/src/copy.c
index 2a804945e..a1dce8e87 100644
--- a/src/copy.c
+++ b/src/copy.c
@@ -53,6 +53,7 @@
#include "ignore-value.h"
#include "ioblksize.h"
#include "quote.h"
+#include "renameat2.h"
#include "root-uid.h"
#include "same.h"
#include "savedir.h"
@@ -1907,44 +1908,62 @@ copy_internal (char const *src_name, char const *dst_name,
bool dereference = should_dereference (x, command_line_arg);
- if (!new_dst)
+ int rename_errno = -1;
+ if (x->move_mode)
{
- /* Regular files can be created by writing through symbolic
- links, but other files cannot. So use stat on the
- destination when copying a regular file, and lstat otherwise.
- However, if we intend to unlink or remove the destination
- first, use lstat, since a copy won't actually be made to the
- destination in that case. */
- bool use_stat =
- ((S_ISREG (src_mode)
- || (x->copy_as_regular
- && ! (S_ISDIR (src_mode) || S_ISLNK (src_mode))))
- && ! (x->move_mode || x->symbolic_link || x->hard_link
- || x->backup_type != no_backups
- || x->unlink_dest_before_opening));
- if ((use_stat
- ? stat (dst_name, &dst_sb)
- : lstat (dst_name, &dst_sb))
+ if (renameat2 (AT_FDCWD, src_name, AT_FDCWD, dst_name, RENAME_NOREPLACE)
!= 0)
+ rename_errno = errno;
+ else
+ {
+ rename_errno = 0;
+ new_dst = true;
+ if (rename_succeeded)
+ *rename_succeeded = true;
+ }
+ }
+
+ if (!new_dst)
+ {
+ if (! (rename_errno == EEXIST && x->interactive == I_ALWAYS_NO))
{
- if (errno != ENOENT)
+ /* Regular files can be created by writing through symbolic
+ links, but other files cannot. So use stat on the
+ destination when copying a regular file, and lstat otherwise.
+ However, if we intend to unlink or remove the destination
+ first, use lstat, since a copy won't actually be made to the
+ destination in that case. */
+ bool use_lstat
+ = ((! S_ISREG (src_mode)
+ && (! x->copy_as_regular
+ || S_ISDIR (src_mode) || S_ISLNK (src_mode)))
+ || x->move_mode || x->symbolic_link || x->hard_link
+ || x->backup_type != no_backups
+ || x->unlink_dest_before_opening);
+ int fstatat_flags = use_lstat ? AT_SYMLINK_NOFOLLOW : 0;
+ if (fstatat (AT_FDCWD, dst_name, &dst_sb, fstatat_flags) == 0)
{
- error (0, errno, _("cannot stat %s"), quoteaf (dst_name));
- return false;
+ have_dst_lstat = use_lstat;
+ rename_errno = EEXIST;
}
else
{
+ if (errno != ENOENT)
+ {
+ error (0, errno, _("cannot stat %s"), quoteaf (dst_name));
+ return false;
+ }
new_dst = true;
}
}
- else
- { /* Here, we know that dst_name exists, at least to the point
- that it is stat'able or lstat'able. */
- bool return_now;
- have_dst_lstat = !use_stat;
- if (! same_file_ok (src_name, &src_sb, dst_name, &dst_sb,
- x, &return_now))
+ if (rename_errno == EEXIST)
+ {
+ bool return_now = false;
+
+ if (x->interactive != I_ALWAYS_NO
+ && ! same_file_ok (src_name, &src_sb, dst_name, &dst_sb,
+ x, &return_now))
{
error (0, 0, _("%s and %s are the same file"),
quoteaf_n (0, src_name), quoteaf_n (1, dst_name));
@@ -2233,7 +2252,9 @@ copy_internal (char const *src_name, char const *dst_name,
Also, with --recursive, record dev/ino of each command-line directory.
We'll use that info to detect this problem: cp -R dir dir. */
- if (x->recursive && S_ISDIR (src_mode))
+ if (rename_errno == 0)
+ earlier_file = NULL;
+ else if (x->recursive && S_ISDIR (src_mode))
{
if (command_line_arg)
earlier_file = remember_copied (dst_name, src_sb.st_ino, src_sb.st_dev);
@@ -2319,7 +2340,10 @@ copy_internal (char const *src_name, char const *dst_name,
if (x->move_mode)
{
- if (rename (src_name, dst_name) == 0)
+ if (rename_errno == EEXIST)
+ rename_errno = rename (src_name, dst_name) == 0 ? 0 : errno;
+
+ if (rename_errno == 0)
{
if (x->verbose)
{
@@ -2356,7 +2380,7 @@ copy_internal (char const *src_name, char const *dst_name,
/* This happens when attempting to rename a directory to a
subdirectory of itself. */
- if (errno == EINVAL)
+ if (rename_errno == EINVAL)
{
/* FIXME: this is a little fragile in that it relies on rename(2)
failing with a specific errno value. Expect problems on
@@ -2391,7 +2415,7 @@ copy_internal (char const *src_name, char const *dst_name,
where you'd replace '18' with the integer in parentheses that
was output from the perl one-liner above.
If necessary, of course, change '/tmp' to some other directory. */
- if (errno != EXDEV)
+ if (rename_errno != EXDEV)
{
/* There are many ways this can happen due to a race condition.
When something happens between the initial XSTAT and the
@@ -2403,7 +2427,7 @@ copy_internal (char const *src_name, char const *dst_name,
If the permissions on the directory containing the source or
destination file are made too restrictive, the rename will
fail. Etc. */
- error (0, errno,
+ error (0, rename_errno,
_("cannot move %s to %s"),
quoteaf_n (0, src_name), quoteaf_n (1, dst_name));
forget_created (src_sb.st_ino, src_sb.st_dev);
--
2.14.3