Subject: bug#10686: mv: moving hardlink of a softlink to the softlink does nothing

From: Jim Meyering <[email-masked]>
Date: Wed, 01 Feb 2012 13:47:44 +0100

Bernhard Voelker wrote:
> Playing around with the latest mv checkin,
> I noticed another corner case:
>
> Create a file 'f', a symlink 'l' to it, and
> then a hardlink 's' to that symlink:
>
>   $ touch f && ln -s f l && ln l s && ls -ogi
>   total 0
>   6444 -rw-r--r-- 1 0 Feb  1 08:52 f
>   6462 lrwxrwxrwx 2 1 Feb  1 08:52 l -> f
>   6462 lrwxrwxrwx 2 1 Feb  1 08:52 s -> f

Hi Bernhard,
Thanks for the report.

Here, you've already gotten into questionable territory, since the
idea of a hard link to a symbolic link is not standardized.
For example, on FreeBSD 9.0, you cannot even create one:
(instead, it hard-links the referent of the symlink)

    freebsd$ touch f && ln -s f l && ln l s && ls -ogi f l s
    1587047 -rw-r--r-- 2 0 Feb  1 04:58 f
    1587100 lrwxr-xr-x 1 1 Feb  1 04:58 l -> f
    1587047 -rw-r--r-- 2 0 Feb  1 04:58 s

> Trying to mv the hardlink over the symlink seems to succeed:
>
>   $ ~/git/coreutils/src/mv s l
>
> ... but the name 's' was not unlinked:

That is definitely undesirable behavior.
For now, one possibility is to make "mv" reject
that corner case with this diagnostic:

    mv: 's' and 'l' are the same file

Proposed patch below.

>   $ ls -ogi
>   total 0
>   6444 -rw-r--r-- 1 0 Feb  1 08:52 f
>   6462 lrwxrwxrwx 2 1 Feb  1 08:52 l -> f
>   6462 lrwxrwxrwx 2 1 Feb  1 08:52 s -> f
>
> Using the -v option looks also normal, but is a nop:
>
>   $ ~/git/coreutils/src/mv -v s l
>   ‘s’ -> ‘l’
>   $ ls -ogi
>   total 0
>   6444 -rw-r--r-- 1 0 Feb  1 08:52 f
>   6462 lrwxrwxrwx 2 1 Feb  1 08:52 l -> f
>   6462 lrwxrwxrwx 2 1 Feb  1 08:52 s -> f
>
> The strace only shows the rename():
>
>   stat("l", {st_mode=S_IFREG|0644, st_size=0, ...}) = 0
>   lstat("s", {st_mode=S_IFLNK|0777, st_size=1, ...}) = 0
>   lstat("l", {st_mode=S_IFLNK|0777, st_size=1, ...}) = 0
>   rename("s", "l")                        = 0
>
>
> I'd have expected either
> a) the name 's' to be unlinked, or
> b) an error message that something was wrong (well, whatever).
> I'd prefer a) of course.

So would I.

> That behaviour didn't change at least since version 7.1
> (on openSuSE-11.3), but back in the earlier days in 5.93
> (SLES-10.3), 's' disappeared as expected:
>
>   stat("l", {st_mode=S_IFREG|0640, st_size=0, ...}) = 0
>   lstat("s", {st_mode=S_IFLNK|0777, st_size=1, ...}) = 0
>   lstat("l", {st_mode=S_IFLNK|0777, st_size=1, ...}) = 0
>   unlink("l")                             = 0
>   rename("s", "l")                        = 0
>
> Does mv now work as specified? Is this a kernel or
> a coreutils (or a user) issue?

It's a standards and kernel issue.
POSIX explicitly says (of rename)

    If the old argument and the new argument resolve to the same existing
    file, rename( ) shall return successfully and perform no other action.

though that particular wording might be slated to change with POSIX 2008,
to allow different (more desirable) behavior.  Search the austin-group
archives if you need to be sure.

Also, I don't know whether the above wording applies to this particular
case of two hard links to the same symlink.  Again, I think we're in
unspecified territory.

Here's a proposed patch.
This is such an improbable corner case that I think it's not worth trying
to fix any other way (either by unlinking the destination or by
replacing rename).

(of course, if I go with this, I'll insert a big comment justifying
this tiny change)

diff --git a/src/copy.c b/src/copy.c
index 4810de8..78cd8c8 100644
--- a/src/copy.c
+++ b/src/copy.c
@@ -1229,7 +1229,7 @@ same_file_ok (char const *src_name, struct stat const *src_sb,
          know this here IFF preserving symlinks), then it's ok -- as long
          as they are distinct.  */
       if (S_ISLNK (src_sb->st_mode) && S_ISLNK (dst_sb->st_mode))
-        return ! same_name (src_name, dst_name);
+        return ! same_name (src_name, dst_name) && ! same_link;

       src_sb_link = src_sb;
       dst_sb_link = dst_sb;