Subject: Re: bug#78509: Coreutils' mv and cp 9.5 do not work properly on old PPC Mac OS X 10.4.11, Tiger

From: Peter Dyballa <[email-masked]>
Date: Fri, 23 May 2025 13:27:44 +0200

When using Gdb I skip in mv.c's main() function over the first few calls and come to the while loop, line #344. It is entered and

    378           target_directory = optarg;.

is – visited? The name "out" contains a "t" which is also listed in the list of command line options. After the while loop target_directory is still 0x0. It also "visits" line #386 because of "u". But this too does not seem to change anything…

But then the if at line

    431   if (no_target_directory)

is reached. no_target_directory is false, so

    443   else if (target_directory)

is reached. target_directory is still 0x0, the "nullptr", so we come to line

    450   else

and then to an if on line

    457       if (x.rename_errno != 0)

x.rename_errno is 17 and therefore != 0, so the if expression is true and its block entered:

    458         {
    459           int fd = target_directory_operand (lastfile, &sb);
    460           if (target_dirfd_valid (fd))
    461             {
    462               x.rename_errno = -1;
    463               target_dirfd = fd;
    464               target_directory = lastfile;
    465               n_files--;
    466             }
    467           else
    468             {
    ...  
    484             }
    485         }

Here x.rename_errno is (re)set to -1 although already != 0, and most importantly target_directory receives value "out". So from now on mv might be sure that something needs to be moved into a *directory* "out", and we come to

    519   if (target_directory)
    520     {
    521       /* Initialize the hash table only if we'll need it.
    522          The problem it is used to detect can arise only if there are
    523          two or more files to move.  */
    524       if (2 <= n_files)
    525         dest_info_init (&x);
    526  
    527       ok = true;
    528       for (int i = 0; i < n_files; ++i)
    529         {
    530           x.last_file = i + 1 == n_files;
    531           char const *source = file[i];
    532           char const *source_basename = last_component (source);
    533           char *dest_relname;
    534           char *dest = file_name_concat (target_directory, source_basename,
    535                                          &dest_relname);
    536           strip_trailing_slashes (dest_relname);
    537           ok &= do_move (source, dest, target_dirfd, dest_relname, &x);
    538           free (dest);
    539         }
    540     }

target_directory is "out" and therefore we enter the true block. n_files has been decremented on line #365, so dest_info_init() is not called and we enter the for loop to do our job…

The cause for faulty behaviour is the value of x.rename_errno. It receives its value from

    450   else
    451     {
    452       char const *lastfile = file[n_files - 1];
    453       if (n_files == 2 && !x.exchange)
    454         x.rename_errno = (renameatu (AT_FDCWD, file[0], AT_FDCWD, lastfile,
    455                                      RENAME_NOREPLACE)
    456                           ? errno : 0);
    457       if (x.rename_errno != 0)

On line # 453 n_files is 2, and x.exchange is false, so we have if( true && true) and renameatu() is called, with:

	renameatu (fd1=-3041965, src=0xbfffd607 "k", fd2=-3041965, dst=0xbfffd609 "out", flags=1) at lib/renameatu.c:257

    254 #else /* !HAVE_RENAMEAT */
    255 
    256   /* RENAME_NOREPLACE is the only flag currently supported.  */
    257   if (flags & ~RENAME_NOREPLACE)
    258     return errno_fail (ENOTSUP);
    259   return at_func2 (fd1, src, fd2, dst, flags ? rename_noreplace : rename);
    260  
    261 #endif /* !HAVE_RENAMEAT */

Mac OS X 10.4, Tiger, does not have the renameat() system call, so not much is used from the GNUlib function.

The if clause on line #257 is false, so renameatu() returns, calling at_func2():

	at_func2 (fd1=-3041965, file1=0xbfffd607 "k", fd2=-3041965, file2=0xbfffd609 "out", func=0x25270 <rename_noreplace>) at lib/at-func2.c:77

     77   if ((fd1 == AT_FDCWD || IS_ABSOLUTE_FILE_NAME (file1))
     78       && (fd2 == AT_FDCWD || IS_ABSOLUTE_FILE_NAME (file2)))
     79     return func (file1, file2); /* Case 0-2, 4-6, 8-10.  */

Here the if clause on lines #77/78 is true and func() is called:

	rename_noreplace (src=0xbfffd607 "k", dst=0xbfffd609 "out") at lib/renameatu.c:55

     39 #if HAVE_RENAMEAT
     40       41 # include <stdlib.h>
     42 # include <string.h>
     43       44 # include "dirname.h"
     45 # include "openat.h"
     46       47 #else
     48 # include "openat-priv.h"
     49       50 static int
     51 rename_noreplace (char const *src, char const *dst)
     52 {
     53   /* This has a race between the call to lstat and the call to rename.  */
     54   struct stat st;
     55   return (lstat (dst, &st) == 0 || errno == EOVERFLOW ? errno_fail (EEXIST)
     56           : errno == ENOENT ? rename (src, dst)
     57           : -1);
     58 }
     59 #endif

Isn't calling rename_noreplace() already faulty? Shouldn't it be rename_and_do_replace_and/or_remove()?

The faulty behaviour starts here in mv.c:

    454         x.rename_errno = (renameatu (AT_FDCWD, file[0], AT_FDCWD, lastfile,
    455                                      RENAME_NOREPLACE)

RENAME_NOREPLACE should not be passed to the function here. Anyway, lib/renameatu.c is divided into two parts, one for systems with renameat() family of library functions and another one without them. And this part might behave differently and take the last parameter as a target directory.

I checked on my up-to-date Mac coreutils 9.7: mv -v k out just works! So I should start to debug here too to understand what produces the different behaviour – but first I need a working version of Gdb!

--
Greetings

  Pete

Clovis' Consideration of an Atmospheric Anomaly:
        The perversity of nature is nowhere better demonstrated than by the fact that, when exposed to the same atmosphere, bread becomes hard while crackers become soft