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

From: Jim Meyering <[email-masked]>
Date: Wed, 01 Feb 2012 16:45:06 +0100

Eric Blake wrote:

> On 02/01/2012 05:47 AM, Jim Meyering wrote:
>> 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.
>
> Actually, POSIX 2008 _did_ standardize the notion of a hard link to a
> symlink, thanks to linkat().
>
>> For example, on FreeBSD 9.0, you cannot even create one:
>
> That's a POSIX-compliance bug in FreeBSD.  In that case, the best we can
> do is emulate it by creating a new symlink with identical contents and
> owner and as much of the timestamp as lutimens will allow.
>
>>
>> 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.
>
> The wording for rename(2) did not change, but the wording for mv(1)
> _did_ change to allow slightly more desirable behavior.  Here's the
> latest wording, as modified by the latest bug report:
>
> http://austingroupbugs.net/view.php?id=534
>
> If the source_file operand and destination path resolve to either the
> same existing directory entry or different directory entries for the
> same existing file, then the destination path shall not be removed, and
> one of the following shall occur:
>  a. No change is made to source_file, no error occurs, and no diagnostic
> is issued.
>  b. No change is made to source_file, a diagnostic is issued to standard
> error identifying the two names, and the exit status is affected.
>  c. If the source_file operand and destination path name distinct
> directory entries, then the source_file operand is removed, no error
> occurs, and no diagnostic is issued.
>
> The mv utility shall do nothing more with the current source_file, and
> go on to any remaining source_files.
>
>> 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.
>
> POSIX actually specifies this quite well - two hardlinks to the same
> symlink qualify as an instance of two file names that resolve to the
> same inode after path resolution as checked with lstat().

Thanks for the clarification and quotes, Eric.

Bernhard, here's a better patch.
With it, mv simply unlinks the "s" in your example:

diff --git a/src/copy.c b/src/copy.c
index 4810de8..73f5cc5 100644
--- a/src/copy.c
+++ b/src/copy.c
@@ -1229,7 +1229,17 @@ 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);
+        {
+          bool sn = same_name (src_name, dst_name);
+          if ( ! sn && same_link)
+            {
+              *unlink_src = true;
+              *return_now = true;
+              return true;
+            }
+
+          return ! sn;
+        }

       src_sb_link = src_sb;
       dst_sb_link = dst_sb;