bug-coreutils
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

bug#33096: [INSTALLED] ln: avoid directory hard-link races


From: Paul Eggert
Subject: bug#33096: [INSTALLED] ln: avoid directory hard-link races
Date: Fri, 19 Oct 2018 12:43:08 -0700

Previously, 'ln A B' did 'stat("B"), lstat("A"), link("A","B")'
where the stat and lstat were necessary to avoid hard-linking
directories on systems that can hard-link directories.
Now, in situations that prohibit hard links to directories,
'ln A B' merely does 'link("A","B")'.  The new behavior
avoids some races and should be more efficient.
This patch was inspired by Bug#10020, which was about 'ln'.
* bootstrap.conf (gnulib_modules): Add unlinkdir.
* src/force-link.c (force_linkat, force_symlinkat): New arg for
error number of previous try.  Return error number, 0, or -1 if
error, success, or success after removal.  All callers changed.
* src/ln.c: Include priv-set.h, unlinkdir.h.
(beware_hard_dir_link): New static var.
(errnoize, atomic_link): New functions.
(target_directory_operand): Use errnoize for simplicity.
(do_link): New arg for error number of previous try.  All callers
changed.  Do each link atomically if possible.
(main): Do -r check earlier.  Remove linkdir privileges so we can
use a single linkat/symlinkat instead of a racy substitute for the
common case of 'ln A B' and 'ln -s A B'.  Set beware_hard_dir_link
to disable this optimization.
---
 NEWS             |   6 +
 bootstrap.conf   |   1 +
 src/copy.c       |  30 ++---
 src/force-link.c |  54 ++++----
 src/force-link.h |   4 +-
 src/ln.c         | 333 ++++++++++++++++++++++++++---------------------
 6 files changed, 239 insertions(+), 189 deletions(-)

diff --git a/NEWS b/NEWS
index 0fea1a118..ae9667e61 100644
--- a/NEWS
+++ b/NEWS
@@ -10,6 +10,12 @@ GNU coreutils NEWS                                    -*- 
outline -*-
 
   df no longer corrupts displayed multibyte characters on macOS.
 
+  When possible 'ln A B' now merely links A to B and reports an error
+  if this fails, instead of statting A and B before linking.  This
+  uses fewer system calls and avoids some races.  The old statting
+  approach is still used in situations where hard links to directories
+  are allowed (e.g., NetBSD when superuser).
+
 ** New features
 
   id now supports specifying multiple users.
diff --git a/bootstrap.conf b/bootstrap.conf
index fcf29dc23..f193a5825 100644
--- a/bootstrap.conf
+++ b/bootstrap.conf
@@ -257,6 +257,7 @@ gnulib_modules="
   unistd-safer
   unlink-busy
   unlinkat
+  unlinkdir
   unlocked-io
   unsetenv
   update-copyright
diff --git a/src/copy.c b/src/copy.c
index 417147bc2..1f4d47737 100644
--- a/src/copy.c
+++ b/src/copy.c
@@ -1783,16 +1783,16 @@ static bool
 create_hard_link (char const *src_name, char const *dst_name,
                   bool replace, bool verbose, bool dereference)
 {
-  int status = force_linkat (AT_FDCWD, src_name, AT_FDCWD, dst_name,
-                             dereference ? AT_SYMLINK_FOLLOW : 0,
-                             replace);
-  if (status < 0)
+  int err = force_linkat (AT_FDCWD, src_name, AT_FDCWD, dst_name,
+                          dereference ? AT_SYMLINK_FOLLOW : 0,
+                          replace, -1);
+  if (0 < err)
     {
-      error (0, errno, _("cannot create hard link %s to %s"),
+      error (0, err, _("cannot create hard link %s to %s"),
              quoteaf_n (0, dst_name), quoteaf_n (1, src_name));
       return false;
     }
-  if (0 < status && verbose)
+  if (err < 0 && verbose)
     printf (_("removed %s\n"), quoteaf (dst_name));
   return true;
 }
@@ -2630,11 +2630,12 @@ copy_internal (char const *src_name, char const 
*dst_name,
               goto un_backup;
             }
         }
-      if (force_symlinkat (src_name, AT_FDCWD, dst_name,
-                           x->unlink_dest_after_failed_open)
-          < 0)
+
+      int err = force_symlinkat (src_name, AT_FDCWD, dst_name,
+                                 x->unlink_dest_after_failed_open, -1);
+      if (0 < err)
         {
-          error (0, errno, _("cannot create symbolic link %s to %s"),
+          error (0, err, _("cannot create symbolic link %s to %s"),
                  quoteaf_n (0, dst_name), quoteaf_n (1, src_name));
           goto un_backup;
         }
@@ -2713,10 +2714,9 @@ copy_internal (char const *src_name, char const 
*dst_name,
           goto un_backup;
         }
 
-      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)
+      int symlink_err = force_symlinkat (src_link_val, AT_FDCWD, dst_name,
+                                         x->unlink_dest_after_failed_open, -1);
+      if (0 < symlink_err && 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.
@@ -2733,7 +2733,7 @@ copy_internal (char const *src_name, char const *dst_name,
             }
         }
       free (src_link_val);
-      if (symlink_err)
+      if (0 < symlink_err)
         {
           error (0, symlink_err, _("cannot create symbolic link %s"),
                  quoteaf (dst_name));
diff --git a/src/force-link.c b/src/force-link.c
index 1794a5786..f2242fc36 100644
--- a/src/force-link.c
+++ b/src/force-link.c
@@ -85,22 +85,27 @@ try_link (char *dest, void *arg)
 
 /* Hard-link directory SRCDIR's file SRCNAME to directory DSTDIR's
    file DSTNAME, using linkat-style FLAGS to control the linking.
-   If FORCE and DSTNAME already exists, replace it atomically.  Return
-   1 if successful and DSTNAME already existed,
+   If FORCE and DSTNAME already exists, replace it atomically.
+   If LINKAT_ERRNO is 0, the hard link is already done; if positive,
+   the hard link was tried and failed with errno == LINKAT_ERRNO.  Return
+   -1 if successful and DSTNAME already existed,
    0 if successful and DSTNAME did not already exist, and
-   -1 (setting errno) on failure.  */
+   a positive errno value on failure.  */
 extern int
 force_linkat (int srcdir, char const *srcname,
-              int dstdir, char const *dstname, int flags, bool force)
+              int dstdir, char const *dstname, int flags, bool force,
+              int linkat_errno)
 {
-  int r = linkat (srcdir, srcname, dstdir, dstname, flags);
-  if (!force || r == 0 || errno != EEXIST)
-    return r;
+  if (linkat_errno < 0)
+    linkat_errno = (linkat (srcdir, srcname, dstdir, dstname, flags) == 0
+                    ? 0 : errno);
+  if (!force || linkat_errno != EEXIST)
+    return linkat_errno;
 
   char buf[smallsize];
   char *dsttmp = samedir_template (dstname, buf);
   if (! dsttmp)
-    return -1;
+    return errno;
   struct link_arg arg = { srcdir, srcname, dstdir, flags };
   int err;
 
@@ -108,7 +113,7 @@ force_linkat (int srcdir, char const *srcname,
     err = errno;
   else
     {
-      err = renameat (dstdir, dsttmp, dstdir, dstname) == 0 ? 0 : errno;
+      err = renameat (dstdir, dsttmp, dstdir, dstname) == 0 ? -1 : errno;
       /* Unlink DSTTMP even if renameat succeeded, in case DSTTMP
          and DSTNAME were already the same hard link and renameat
          was a no-op.  */
@@ -117,10 +122,7 @@ force_linkat (int srcdir, char const *srcname,
 
   if (dsttmp != buf)
     free (dsttmp);
-  if (!err)
-    return 1;
-  errno = err;
-  return -1;
+  return err;
 }
 
 
@@ -140,22 +142,25 @@ try_symlink (char *dest, void *arg)
 }
 
 /* Create a symlink containing SRCNAME in directory DSTDIR's file DSTNAME.
-   If FORCE and DSTNAME already exists, replace it atomically.  Return
-   1 if successful and DSTNAME already existed,
+   If FORCE and DSTNAME already exists, replace it atomically.
+   If SYMLINKAT_ERRNO is 0, the symlink is already done; if positive,
+   the symlink was tried and failed with errno == SYMLINKAT_ERRNO.  Return
+   -1 if successful and DSTNAME already existed,
    0 if successful and DSTNAME did not already exist, and
-   -1 (setting errno) on failure.  */
+   a positive errno value on failure.  */
 extern int
 force_symlinkat (char const *srcname, int dstdir, char const *dstname,
-                 bool force)
+                 bool force, int symlinkat_errno)
 {
-  int r = symlinkat (srcname, dstdir, dstname);
-  if (!force || r == 0 || errno != EEXIST)
-    return r;
+  if (symlinkat_errno < 0)
+    symlinkat_errno = symlinkat (srcname, dstdir, dstname) == 0 ? 0 : errno;
+  if (!force || symlinkat_errno != EEXIST)
+    return symlinkat_errno;
 
   char buf[smallsize];
   char *dsttmp = samedir_template (dstname, buf);
   if (!dsttmp)
-    return -1;
+    return errno;
   struct symlink_arg arg = { srcname, dstdir };
   int err;
 
@@ -170,13 +175,10 @@ force_symlinkat (char const *srcname, int dstdir, char 
const *dstname,
     {
       /* Don't worry about renameat being a no-op, since DSTTMP is
          newly created.  */
-      err = 0;
+      err = -1;
     }
 
   if (dsttmp != buf)
     free (dsttmp);
-  if (!err)
-    return 1;
-  errno = err;
-  return -1;
+  return err;
 }
diff --git a/src/force-link.h b/src/force-link.h
index 7ea3817de..595c93f1a 100644
--- a/src/force-link.h
+++ b/src/force-link.h
@@ -1,2 +1,2 @@
-extern int force_linkat (int, char const *, int, char const *, int, bool);
-extern int force_symlinkat (char const *, int, char const *, bool);
+extern int force_linkat (int, char const *, int, char const *, int, bool, int);
+extern int force_symlinkat (char const *, int, char const *, bool, int);
diff --git a/src/ln.c b/src/ln.c
index 7bbd42f71..9f197a4b7 100644
--- a/src/ln.c
+++ b/src/ln.c
@@ -30,8 +30,10 @@
 #include "force-link.h"
 #include "hash.h"
 #include "hash-triple.h"
+#include "priv-set.h"
 #include "relpath.h"
 #include "same.h"
+#include "unlinkdir.h"
 #include "yesno.h"
 #include "canonicalize.h"
 
@@ -69,6 +71,9 @@ static bool verbose;
    directories on most existing systems (Solaris being an exception).  */
 static bool hard_dir_link;
 
+/* If true, watch out for creating or removing hard links to directories.  */
+static bool beware_hard_dir_link;
+
 /* If nonzero, and the specified destination is a symbolic link to a
    directory, treat it just as if it were a directory.  Otherwise, the
    command 'ln --force --no-dereference file symlink-to-dir' deletes
@@ -113,6 +118,14 @@ errno_nonexisting (int err)
   return err == ENOENT || err == ENAMETOOLONG || err == ENOTDIR || err == 
ELOOP;
 }
 
+/* Return an errno value for a system call that returned STATUS.
+   This is zero if STATUS is zero, and is errno otherwise.  */
+
+static int
+errnoize (int status)
+{
+  return status < 0 ? errno : 0;
+}
 
 /* FILE is the last operand of this command.  Return true if FILE is a
    directory.  But report an error if there is a problem accessing FILE,
@@ -126,9 +139,8 @@ target_directory_operand (char const *file)
   size_t blen = strlen (b);
   bool looks_like_a_dir = (blen == 0 || ISSLASH (b[blen - 1]));
   struct stat st;
-  int stat_result =
-    (dereference_dest_dir_symlinks ? stat (file, &st) : lstat (file, &st));
-  int err = (stat_result == 0 ? 0 : errno);
+  int flags = dereference_dest_dir_symlinks ? 0 : AT_SYMLINK_NOFOLLOW;
+  int err = errnoize (fstatat (AT_FDCWD, file, &st, flags));
   bool is_a_dir = !err && S_ISDIR (st.st_mode);
   if (err && ! errno_nonexisting (errno))
     die (EXIT_FAILURE, err, _("failed to access %s"), quoteaf (file));
@@ -171,162 +183,181 @@ convert_abs_rel (const char *from, const char *target)
   return relative_from ? relative_from : xstrdup (from);
 }
 
+/* Link SOURCE to DEST atomically.  Return 0 if successful, a positive
+   errno value on failure, and -1 if an atomic link cannot be done.
+   This handles the common case where DEST does not already exist and
+   -r is not specified.  */
+
+static int
+atomic_link (char const *source, char const *dest)
+{
+  return (symbolic_link
+          ? (relative ? -1 : errnoize (symlink (source, dest)))
+          : beware_hard_dir_link
+          ? -1
+          : errnoize (linkat (AT_FDCWD, source, AT_FDCWD, dest,
+                              logical ? AT_SYMLINK_FOLLOW : 0)));
+}
+
 /* Make a link DEST to the (usually) existing file SOURCE.
    Symbolic links to nonexistent files are allowed.
+   LINK_ERRNO is zero if the link has already been made,
+   positive if attempting the link failed with errno == LINK_ERRNO,
+   -1 if no attempt has been made to create the link.
    Return true if successful.  */
 
 static bool
-do_link (const char *source, const char *dest)
+do_link (const char *source, const char *dest, int link_errno)
 {
   struct stat source_stats;
-  struct stat dest_stats;
+  int source_status = 1;
   char *dest_backup = NULL;
   char *rel_source = NULL;
-  bool dest_lstat_ok = false;
-  bool source_is_dir = false;
+  int nofollow_flag = logical ? 0 : AT_SYMLINK_NOFOLLOW;
+  if (link_errno < 0)
+    link_errno = atomic_link (source, dest);
 
-  if (!symbolic_link)
+  /* Get SOURCE_STATS if later code will need it, if only for sharper
+     diagnostics.  */
+  if ((link_errno || dest_set) && !symbolic_link)
     {
-       /* Which stat to use depends on whether linkat will follow the
-          symlink.  We can't use the shorter
-          (logical?stat:lstat) (source, &source_stats)
-          since stat might be a function-like macro.  */
-      if ((logical ? stat (source, &source_stats)
-           : lstat (source, &source_stats))
-          != 0)
+      source_status = fstatat (AT_FDCWD, source, &source_stats, nofollow_flag);
+      if (source_status != 0)
         {
           error (0, errno, _("failed to access %s"), quoteaf (source));
           return false;
         }
-
-      if (S_ISDIR (source_stats.st_mode))
-        {
-          source_is_dir = true;
-          if (! hard_dir_link)
-            {
-              error (0, 0, _("%s: hard link not allowed for directory"),
-                     quotef (source));
-              return false;
-            }
-        }
     }
 
-  if (remove_existing_files || interactive || backup_type != no_backups)
+  if (link_errno)
     {
-      dest_lstat_ok = (lstat (dest, &dest_stats) == 0);
-      if (!dest_lstat_ok && errno != ENOENT)
+      if (!symbolic_link && !hard_dir_link && S_ISDIR (source_stats.st_mode))
         {
-          error (0, errno, _("failed to access %s"), quoteaf (dest));
+          error (0, 0, _("%s: hard link not allowed for directory"),
+                 quotef (source));
           return false;
         }
-    }
-
-  /* If the current target was created as a hard link to another
-     source file, then refuse to unlink it.  */
-  if (dest_lstat_ok
-      && dest_set != NULL
-      && seen_file (dest_set, dest, &dest_stats))
-    {
-      error (0, 0,
-             _("will not overwrite just-created %s with %s"),
-             quoteaf_n (0, dest), quoteaf_n (1, source));
-      return false;
-    }
-
-  /* If --force (-f) has been specified without --backup, then before
-     making a link ln must remove the destination file if it exists.
-     (with --backup, it just renames any existing destination file)
-     But if the source and destination are the same, don't remove
-     anything and fail right here.  */
-  if ((remove_existing_files
-       /* Ensure that "ln --backup f f" fails here, with the
-          "... same file" diagnostic, below.  Otherwise, subsequent
-          code would give a misleading "file not found" diagnostic.
-          This case is different than the others handled here, since
-          the command in question doesn't use --force.  */
-       || (!symbolic_link && backup_type != no_backups))
-      && dest_lstat_ok
-      /* Allow 'ln -sf --backup k k' to succeed in creating the
-         self-referential symlink, but don't allow the hard-linking
-         equivalent: 'ln -f k k' (with or without --backup) to get
-         beyond this point, because the error message you'd get is
-         misleading.  */
-      && (backup_type == no_backups || !symbolic_link)
-      && (!symbolic_link || stat (source, &source_stats) == 0)
-      && SAME_INODE (source_stats, dest_stats)
-      /* The following detects whether removing DEST will also remove
-         SOURCE.  If the file has only one link then both are surely
-         the same link.  Otherwise check whether they point to the same
-         name in the same directory.  */
-      && (source_stats.st_nlink == 1 || same_name (source, dest)))
-    {
-      error (0, 0, _("%s and %s are the same file"),
-             quoteaf_n (0, source), quoteaf_n (1, dest));
-      return false;
-    }
 
-  if (dest_lstat_ok)
-    {
-      if (S_ISDIR (dest_stats.st_mode))
-        {
-          error (0, 0, _("%s: cannot overwrite directory"), quotef (dest));
-          return false;
-        }
-      if (interactive)
-        {
-          fprintf (stderr, _("%s: replace %s? "), program_name, quoteaf 
(dest));
-          if (!yesno ())
-            return true;
-          remove_existing_files = true;
-        }
+      if (relative)
+        source = rel_source = convert_abs_rel (source, dest);
 
-      if (backup_type != no_backups)
+      bool force = (remove_existing_files || interactive
+                    || backup_type != no_backups);
+      if (force)
         {
-          dest_backup = find_backup_file_name (dest, backup_type);
-          if (rename (dest, dest_backup) != 0)
+          struct stat dest_stats;
+          if (lstat (dest, &dest_stats) != 0)
             {
-              int rename_errno = errno;
-              free (dest_backup);
-              dest_backup = NULL;
-              if (rename_errno != ENOENT)
+              if (errno != ENOENT)
                 {
-                  error (0, rename_errno, _("cannot backup %s"),
-                         quoteaf (dest));
+                  error (0, errno, _("failed to access %s"), quoteaf (dest));
                   return false;
                 }
+              force = false;
+            }
+          else if (S_ISDIR (dest_stats.st_mode))
+            {
+              error (0, 0, _("%s: cannot overwrite directory"), quotef (dest));
+              return false;
+            }
+          else if (seen_file (dest_set, dest, &dest_stats))
+            {
+              /* The current target was created as a hard link to another
+                 source file.  */
+              error (0, 0,
+                     _("will not overwrite just-created %s with %s"),
+                     quoteaf_n (0, dest), quoteaf_n (1, source));
+              return false;
+            }
+          else
+            {
+              /* Beware removing DEST if it is the same directory entry as
+                 SOURCE, because in that case removing DEST can cause the
+                 subsequent link creation either to fail (for hard links), or
+                 to replace a non-symlink DEST with a self-loop (for symbolic
+                 links) which loses the contents of DEST.  So, when backing
+                 up, worry about creating hard links (since the backups cover
+                 the symlink case); otherwise, worry about about -f.  */
+              if (backup_type != no_backups
+                  ? !symbolic_link
+                  : remove_existing_files)
+                {
+                  /* Detect whether removing DEST would also remove SOURCE.
+                     If the file has only one link then both are surely the
+                     same directory entry.  Otherwise check whether they point
+                     to the same name in the same directory.  */
+                  if (source_status != 0)
+                    source_status = stat (source, &source_stats);
+                  if (source_status == 0
+                      && SAME_INODE (source_stats, dest_stats)
+                      && (source_stats.st_nlink == 1
+                          || same_name (source, dest)))
+                    {
+                      error (0, 0, _("%s and %s are the same file"),
+                             quoteaf_n (0, source), quoteaf_n (1, dest));
+                      return false;
+                    }
+                }
+
+              if (link_errno < 0 || link_errno == EEXIST)
+                {
+                  if (interactive)
+                    {
+                      fprintf (stderr, _("%s: replace %s? "),
+                               program_name, quoteaf (dest));
+                      if (!yesno ())
+                        return true;
+                    }
+
+                  if (backup_type != no_backups)
+                    {
+                      dest_backup = find_backup_file_name (dest, backup_type);
+                      if (rename (dest, dest_backup) != 0)
+                        {
+                          int rename_errno = errno;
+                          free (dest_backup);
+                          dest_backup = NULL;
+                          if (rename_errno != ENOENT)
+                           {
+                              error (0, rename_errno, _("cannot backup %s"),
+                                     quoteaf (dest));
+                              return false;
+                            }
+                          force = false;
+                        }
+                    }
+                }
             }
         }
+
+      /* If the attempt to create a link fails and we are removing or
+         backing up destinations, unlink the destination and try again.
+
+         On the surface, POSIX states that 'ln -f A B' unlinks B before trying
+         to link A to B.  But strictly following this has the counterintuitive
+         effect of losing the contents of B if A does not exist.  Fortunately,
+         POSIX 2008 clarified that an application is free to fail early if it
+         can prove that continuing onwards cannot succeed, so we can try to
+         link A to B before blindly unlinking B, thus sometimes attempting to
+         link a second time during a successful 'ln -f A B'.
+
+         Try to unlink DEST even if we may have backed it up successfully.
+         In some unusual cases (when DEST and DEST_BACKUP are hard-links
+         that refer to the same file), rename succeeds and DEST remains.
+         If we didn't remove DEST in that case, the subsequent symlink or
+         link call would fail.  */
+      link_errno
+        = (symbolic_link
+           ? force_symlinkat (source, AT_FDCWD, dest, force, link_errno)
+           : force_linkat (AT_FDCWD, source, AT_FDCWD, dest,
+                           logical ? AT_SYMLINK_FOLLOW : 0,
+                           force, link_errno));
+      /* Until now, link_errno < 0 meant the link has not been tried.
+         From here on, link_errno < 0 means the link worked but
+         required removing the destination first.  */
     }
 
-  if (relative)
-    source = rel_source = convert_abs_rel (source, dest);
-
-  /* If the attempt to create a link fails and we are removing or
-     backing up destinations, unlink the destination and try again.
-
-     On the surface, POSIX describes an algorithm that states that
-     'ln -f A B' will call unlink() on B before ever attempting
-     link() on A.  But strictly following this has the counterintuitive
-     effect of losing the contents of B, if A does not exist.
-     Fortunately, POSIX 2008 clarified that an application is free
-     to fail early if it can prove that continuing onwards cannot
-     succeed, so we are justified in trying link() before blindly
-     removing B, thus sometimes calling link() a second time during
-     a successful 'ln -f A B'.
-
-     Try to unlink DEST even if we may have backed it up successfully.
-     In some unusual cases (when DEST and DEST_BACKUP are hard-links
-     that refer to the same file), rename succeeds and DEST remains.
-     If we didn't remove DEST in that case, the subsequent symlink or link
-     call would fail.  */
-  bool ok_to_remove = remove_existing_files || dest_backup;
-  bool ok = 0 <= (symbolic_link
-                  ? force_symlinkat (source, AT_FDCWD, dest, ok_to_remove)
-                  : force_linkat (AT_FDCWD, source, AT_FDCWD, dest,
-                                  logical ? AT_SYMLINK_FOLLOW : 0,
-                                  ok_to_remove));
-
-  if (ok)
+  if (link_errno <= 0)
     {
       /* Right after creating a hard link, do this: (note dest name and
          source_stats, which are also the just-linked-destinations stats) */
@@ -343,15 +374,15 @@ do_link (const char *source, const char *dest)
     }
   else
     {
-      error (0, errno,
+      error (0, link_errno,
              (symbolic_link
-              ? (errno != ENAMETOOLONG && *source
+              ? (link_errno != ENAMETOOLONG && *source
                  ? _("failed to create symbolic link %s")
                  : _("failed to create symbolic link %s -> %s"))
-              : (errno == EMLINK && !source_is_dir
+              : (link_errno == EMLINK
                  ? _("failed to create hard link to %.0s%s")
-                 : (errno == EDQUOT || errno == EEXIST || errno == ENOSPC
-                    || errno == EROFS)
+                 : (link_errno == EDQUOT || link_errno == EEXIST
+                    || link_errno == ENOSPC || link_errno == EROFS)
                  ? _("failed to create hard link %s")
                  : _("failed to create hard link %s => %s"))),
              quoteaf_n (0, dest), quoteaf_n (1, source));
@@ -365,7 +396,7 @@ do_link (const char *source, const char *dest)
 
   free (dest_backup);
   free (rel_source);
-  return ok;
+  return link_errno <= 0;
 }
 
 void
@@ -444,6 +475,7 @@ main (int argc, char **argv)
   bool no_target_directory = false;
   int n_files;
   char **file;
+  int link_errno = -1;
 
   initialize_main (&argc, &argv);
   set_program_name (argv[0]);
@@ -535,6 +567,15 @@ main (int argc, char **argv)
       usage (EXIT_FAILURE);
     }
 
+  if (relative && !symbolic_link)
+    die (EXIT_FAILURE, 0, _("cannot do --relative without --symbolic"));
+
+  if (!hard_dir_link)
+    {
+      priv_set_remove_linkdir ();
+      beware_hard_dir_link = !cannot_unlink_dir ();
+    }
+
   if (no_target_directory)
     {
       if (target_directory)
@@ -556,11 +597,17 @@ main (int argc, char **argv)
     {
       if (n_files < 2)
         target_directory = ".";
-      else if (2 <= n_files && target_directory_operand (file[n_files - 1]))
-        target_directory = file[--n_files];
-      else if (2 < n_files)
-        die (EXIT_FAILURE, 0, _("target %s is not a directory"),
-             quoteaf (file[n_files - 1]));
+      else
+        {
+          if (n_files == 2)
+            link_errno = atomic_link (file[0], file[1]);
+          if ((link_errno < 0 || link_errno == EEXIST || link_errno == ENOTDIR)
+              && target_directory_operand (file[n_files - 1]))
+            target_directory = file[--n_files];
+          else if (2 < n_files)
+            die (EXIT_FAILURE, 0, _("target %s is not a directory"),
+                 quoteaf (file[n_files - 1]));
+        }
     }
 
   backup_type = (make_backups
@@ -568,12 +615,6 @@ main (int argc, char **argv)
                  : no_backups);
   set_simple_backup_suffix (backup_suffix);
 
-  if (relative && !symbolic_link)
-    {
-        die (EXIT_FAILURE, 0,
-             _("cannot do --relative without --symbolic"));
-    }
-
 
   if (target_directory)
     {
@@ -607,12 +648,12 @@ main (int argc, char **argv)
                                          last_component (file[i]),
                                          &dest_base);
           strip_trailing_slashes (dest_base);
-          ok &= do_link (file[i], dest);
+          ok &= do_link (file[i], dest, -1);
           free (dest);
         }
     }
   else
-    ok = do_link (file[0], file[1]);
+    ok = do_link (file[0], file[1], link_errno);
 
   return ok ? EXIT_SUCCESS : EXIT_FAILURE;
 }
-- 
2.17.2






reply via email to

[Prev in Thread] Current Thread [Next in Thread]