Subject: bug#6960: mv refuses to move a symlink over a hard link to the same file

From: Jim Meyering <[email-masked]>
Date: Wed, 04 Jan 2012 23:30:16 +0100

Anders Kaseorg wrote:

> This refusal makes it impossible to overwrite a hard link with a symlink
> _atomically_.
>
> See for example http://bugs.debian.org/654596 .
>
> In reply to message #17:
>> One may argue that there is no data loss when the destination link count
>> is 2 or more, but once the destination has been unlinked, it may be very
>> challenging to locate another copy.
>
> I would instead argue that there is no data loss when replacing a hard
> link foo with a symlink to bar, as long as foo and bar are _different_
> hard links to the same inode.  In that case, locating the other copy is
> not a problem because the symlink will still be valid.
>
> For example, in the example from the original report:
>   -rw------- 2 matt matt 0 2010-08-31 17:10 New_York
>   -rw------- 2 matt matt 0 2010-08-31 17:10 localtime
>   lrwxrwxrwx 1 matt matt 8 2010-08-31 17:11 localtime.new -> New_York
> mv may reasonably refuse to overwrite New_York with localtime.new, but it
> should not refuse to overwrite localtime with localtime.new.

This sounds reasonable.
Implementing it may even be easy -- for some inputs.
However, in general, we'll have to find a way to accept this:

    mv localtime.new localtime

without also accepting this:

    mv localtime.new New_York

That latter command would leave New_York as a symlink to itself,
with the sole remaining link being "localtime".

Any solution must ensure that the destination and the referent of
source symlink are not the same entry.  We even have a function for
that, once you find the referent's name: same_name (in lib/same.c).
Sounds trivial, but the catch is how to find the referent's name *in
general*.  What if localtime.new points to a symlink, foo->bar->New_York ?
Then, in order to answer that question, we have to be able to traverse
an arbitrarily-long chain of symlinks.  What if there's a loop by the
time we start traversing?  None of these are insurmountable, but they
give you an idea of the amount of complexity that seems (at least to me)
to be required.

You could form the symlink-free full name of the referent, abs_src
and then test same_name (abs_src, dst_name).

I've just done it:
With this patch, cp still passes all of coreutils tests
as well as the one described in the comments below:
(take this with a grain of salt -- it's seen only minimal testing so far)

diff --git a/src/copy.c b/src/copy.c
index 4255d74..41ee3a6 100644
--- a/src/copy.c
+++ b/src/copy.c
@@ -34,6 +34,7 @@
 #include "acl.h"
 #include "backupfile.h"
 #include "buffer-lcm.h"
+#include "canonicalize.h"
 #include "copy.h"
 #include "cp-hash.h"
 #include "extent-scan.h"
@@ -1349,6 +1350,37 @@ same_file_ok (char const *src_name, struct stat const *src_sb,
         }
     }

+  /* In move mode, when
+     src is a symlink,
+     dest is not a symlink,
+     dest has a link count of 2 or more and
+     dest and the referent of src are not the same entry (Hard part),
+     then it's ok, since while we'll lose one of those hard links,
+     src will still point to a remaining link.
+
+     Given this,
+       $ touch f && ln f l && ln -s f s
+       $ ls -og f l s
+       -rw-------. 2  0 Jan  4 22:46 f
+       -rw-------. 2  0 Jan  4 22:46 l
+       lrwxrwxrwx. 1  1 Jan  4 22:46 s -> f
+     this must fail: mv s f
+     this must succeed: mv s l */
+  if (x->dereference == DEREF_NEVER /* FIXME, reconsider this part */
+      && x->move_mode
+      && S_ISLNK (src_sb->st_mode)
+      && ! S_ISLNK (dst_sb->st_mode)
+      && 1 < dst_sb_link->st_nlink)
+    {
+      char *abs_src = canonicalize_file_name (src_name);
+      if (abs_src)
+        {
+          bool result = ! same_name (abs_src, dst_name);
+          free (abs_src);
+          return result;
+        }
+    }
+
   /* 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.  */