Subject: bug#8419: closed (Re: bug#8419: cp -au : New hard links in source becomes new files at destination when using cp -au)

From: [email-masked] (GNU bug Tracking System)
Date: Mon, 25 Jul 2011 11:43:02 +0000

Your bug report

#8419: cp -au : New hard links in source becomes new files at destination when using cp -au

which was filed against the coreutils package, has been closed.

The explanation is attached below, along with your original report.
If you require more details, please reply to [email-masked].

-- 
8419: http://debbugs.gnu.org/cgi/bugreport.cgi?bug=8419
GNU Bug Tracking System
Contact [email-masked] with problems

Subject: Re: bug#8419: cp -au : New hard links in source becomes new files at destination when using cp -au
From: Jim Meyering <[email-masked]>
To: [email-masked]
Cc: [email-masked], [email-masked]
Date: Mon, 25 Jul 2011 13:42:24 +0200
List-Id: <debbugs-submit.debbugs.gnu.org>

[email-masked] wrote:
> I have tried to use the command cp combining the -a and the -u options.
> I had to stop the copying process midways and restarted it again, and to my
> suprice the diskusage at the destination was 10 -20 % larger than the
> diskusage at the source and my disks ran full even though the destination
> disks was the same size as the source disks.
...

Thank you for a fine bug report.
That is indeed a bug, and it affects the latest release, coreutils-8.12.
I confirmed that it afflicts fileutils-3.16 too, so this bug has probably
been present since the initial implementation.

Here's the fix I expect to push:

>From 3095daab7f6d7980b77e01d97d75e702ce4a2e63 Mon Sep 17 00:00:00 2001
From: Jim Meyering <[email-masked]>
Date: Mon, 25 Jul 2011 11:31:01 +0200
Subject: [PATCH] cp -up: preserve all hard links

* src/copy.c (copy_internal): With --update (-u), this function would
return early once it found that the destination is not older than the
source, *without* recording the source-dev/ino--to--dest_name mapping.
That mapping is required in order to preserve src hard links in the
destination tree, so when using cp with --update and --preserve=links
(perhaps via -p or -a), cp could fail to preserve one hard link
per inode when at least one of the hard-linked names already exists
in the destination tree.
Reported by Odd Harry Mannsverk in http://debbugs.gnu.org/8419.
* tests/cp/preserve-link: New file.  Exercise the flaw/fix.
* tests/Makefile.am (TESTS): Add it.
* NEWS (Bug fixes): Mention it.
---
 NEWS                   |    6 ++++++
 src/copy.c             |   12 ++++++++++++
 tests/Makefile.am      |    1 +
 tests/cp/preserve-link |   40 ++++++++++++++++++++++++++++++++++++++++
 4 files changed, 59 insertions(+), 0 deletions(-)
 create mode 100755 tests/cp/preserve-link

diff --git a/NEWS b/NEWS
index 0720719..416060f 100644
--- a/NEWS
+++ b/NEWS
@@ -8,6 +8,12 @@ GNU coreutils NEWS                                    -*- outline -*-
   I.E. for skipped files, the original ownership is output, not the new one.
   [bug introduced in sh-utils-2.0g]

+  cp -u -p would fail to preserve one hard link for each up-to-date copy
+  of a src-hard-linked name in the destination tree.  I.e., if s/a and s/b
+  are hard-linked and dst/s/a is up to date, "cp -up s dst" would copy s/b
+  to dst/s/b rather than simply linking dst/s/b to dst/s/a.
+  [This bug appears to have been present in "the beginning".]
+
   printf '%d' '"' no longer accesses out-of-bounds memory in the diagnostic.
   [bug introduced in sh-utils-1.16]

diff --git a/src/copy.c b/src/copy.c
index c17b942..df8b1db 100644
--- a/src/copy.c
+++ b/src/copy.c
@@ -1628,6 +1628,17 @@ copy_internal (char const *src_name, char const *dst_name,
                      end up removing the source file.  */
                   if (rename_succeeded)
                     *rename_succeeded = true;
+
+                  /* However, we still must record that we've processed
+                     this src/dest pair, in case this source file is
+                     hard-linked to another one.  In that case, we'll use
+                     the mapping information to link the corresponding
+                     destination names.  */
+                  earlier_file = remember_copied (dst_name, src_sb.st_ino,
+                                                  src_sb.st_dev);
+                  if (earlier_file)
+                    goto create_hard_link;
+
                   return true;
                 }
             }
@@ -1948,6 +1959,7 @@ copy_internal (char const *src_name, char const *dst_name,
         }
       else
         {
+        create_hard_link:;
           /* We want to guarantee that symlinks are not followed.  */
           bool link_failed = (linkat (AT_FDCWD, earlier_file, AT_FDCWD,
                                       dst_name, 0) != 0);
diff --git a/tests/Makefile.am b/tests/Makefile.am
index ebd1b11..0a83dae 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -341,6 +341,7 @@ TESTS =						\
   cp/parent-perm-race				\
   cp/perm					\
   cp/preserve-2					\
+  cp/preserve-link				\
   cp/preserve-slink-time			\
   cp/proc-short-read				\
   cp/proc-zero-len				\
diff --git a/tests/cp/preserve-link b/tests/cp/preserve-link
new file mode 100755
index 0000000..d0da873
--- /dev/null
+++ b/tests/cp/preserve-link
@@ -0,0 +1,40 @@
+#!/bin/sh
+# Exercise the fix for http://debbugs.gnu.org/8419
+
+# Copyright (C) 2011 Free Software Foundation, Inc.
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+. "${srcdir=.}/init.sh"; path_prepend_ ../src
+print_ver_ cp
+
+same_inode()
+{
+  local u v
+  u=$(stat --format %i "$1") &&
+    v=$(stat --format %i "$2") && test "$u" = "$v"
+}
+
+mkdir -p s t/s || framework_failure_
+touch s/f t/s/f || framework_failure_
+ln s/f s/link || framework_failure_
+
+# This must create a hard link, t/s/link, to the existing file, t/s/f.
+# With cp from coreutils-8.12 and prior, it would mistakenly copy
+# the file rather than creating the link.
+cp -au s t || fail=1
+
+same_inode t/s/f t/s/link || fail=1
+
+Exit $fail
--
1.7.6.609.gbf6a9

Subject: cp -au : New hard links in source becomes new files at destination when using cp -au
From: [email-masked]
To: [email-masked]
Cc: [email-masked]
Date: Mon, 4 Apr 2011 11:29:00 +0200
List-Id: <debbugs-submit.debbugs.gnu.org>

Hi

I have tried to use the command cp combining the -a and the -u options.
I had to stop the copying process midways and restarted it again, and to my
suprice the diskusage at the destination was 10 -20 % larger than the
diskusage at the source and my disks ran full even though the destination
disks was the same size as the source disks.

My disks contained rsync snapshot backup data so every inod had typically
15 hard links to it.
I found out that when I ran cp -au to update the target, cp -au creates new
files in the destination when there is a new hard link in the source to a
file already existing in the destination directory.
This is not the behaviour I expected.

I got the same behavior on:
SUSE Linux Enterprise Server 11 (x86_64)
and
Ubuntu 10.04

I have included a series of commands to:
    create a source directory with hard linked files,
    make folder target, and copy the directory to this folder
    add a new hard link in the source folder
    update the target folder with the command cp -au
    listing the directory contents of source and target to show that the
source contains 3 hard links to the same file, in the target directory
hardlink2 has become a separate identical file.

If you disagree with me that this is not the desired behaviour of cp -au,
please let me know why.
If you agree, I hope the behaviour of cp -au will change some time in the
future.

regards
Odd Harry Mannsverk

> mkdir source
> touch source/file1
> ln  source/file1 source/hardlink1
> mkdir target
> cp -a source target
> ln  source/file1 source/hardlink2
> cp -au source target
> ls -1iR source
source:
229418 file1
229418 hardlink1
229418 hardlink2
> ls -1iR target
target:
229414 source

target/source:
229415 file1
229415 hardlink1
229416 hardlink2