Subject: Re: bug#79433: 9.7: cp(1): lchownat(2/3) fallback bogus?

From: Steffen Nurpmeso <[email-masked]>
Date: Wed, 17 Sep 2025 18:46:11 +0200

Hello.

Paul Eggert wrote in
 <[email-masked]>:
 |On 2025-09-16 11:51, Steffen Nurpmeso wrote:
 |> I attach a tar archives of the two different logfiles, maybe some
 |> GNU build system guru can figure out more.
 |
 |The first difference in those logs occurs when 'configure' compiles and 
 |run the following program. It should succeed (exit status 0) but in your 

Sure.  (Just wondering, the final cp(1) should work for all
configurable cases if config succeeds; the bug report as such was
that "cp -a symlink symlink" causes a "set -e" build script to
fail due to the exit status.)

 |fakeroot build it fails and 'configure' then assumes chown needs 

fakeroot is btw

  Version:      1.37.1.2
  URL:          http://packages.debian.org/unstable/utils/fakeroot

 |replacement. Can you figure out why the test program fails (exit status 
 |5) in the fakeroot environment? You can use strace to help find out. 
 |Once we figure this out, it may explain the other differences in the logs.
 |
 |   #include <unistd.h>
 |   #include <stdlib.h>
 |   #include <errno.h>
 |   #include <fcntl.h>
 |   #include <sys/stat.h>
 |
 |   int
 |   main (void)
 |   {
 |     struct stat st1, st2;
 |     if (close (creat ("conftest.file", 0600))) return 1;
 |     if (stat ("conftest.file", &st1)) return 2;
 |     sleep (1);
 |     if (chown ("conftest.file", st1.st_uid, st1.st_gid)) return 3;
 |     if (stat ("conftest.file", &st2)) return 4;
 |     if (st2.st_ctime <= st1.st_ctime) return 5;
 |}
 |
 |Here's what strace says on my Fedora 42 host:
 |
 |   creat("conftest.file", 0600) = 3
 |   close(3) = 0
 |   newfstatat(AT_FDCWD, "conftest.file", {st_mode=S_IFREG|0600, 
 |st_size=0, ...}, 0) = 0
 |   clock_nanosleep(CLOCK_REALTIME, 0, {tv_sec=1, tv_nsec=0}, 
 |0x7fffc15bb1f0) = 0
 |   chown("conftest.file", 1000, 1000) = 0
 |   newfstatat(AT_FDCWD, "conftest.file", {st_mode=S_IFREG|0600, 
 |st_size=0, ...}, 0) = 0
 |   exit_group(0) = ?

But that is not via fakeroot?  I get the very result here.
With fakeroot, i have instrumented it, and the above:

       struct stat st1, st2;
       if (close (creat ("conftest.file", 0600))) return 1;
  fprintf(stderr, "AFTER CLOSE\n");
       if (stat ("conftest.file", &st1)) return 2;
  fprintf(stderr, "AFTER STAT\n");
       sleep (1);
       if (chown ("conftest.file", st1.st_uid, st1.st_gid)) return 3;
  fprintf(stderr, "AFTER CHOWN\n");
       if (stat ("conftest.file", &st2)) return 4;
  fprintf(stderr, "AFTER STAT 2\n");
       if (st2.st_ctime == st1.st_ctime) return 6;
       if (st2.st_ctime <= st1.st_ctime) return 5;
  fprintf(stderr, "AFTER TIMECMP\n");
  return 0;

and i show my complete prompt which includes $?

  AFTER CLOSE
  FAKEROOT: r=1092, received message type=1, message=3
  FAKEROOT: process stat oldstate=dev:ino=(21:285264), mode=0100600, own=(1000,1000), nlink=1, rdev=0
  FAKEROOT:    (previously unknown)
  AFTER STAT
  chown path conftest.file owner 0 group 0
       ==lchown -1 1 Operation not permitted
  FAKEROOT: r=1092, received message type=1, message=0
  AFTER CHOWN
  FAKEROOT: process chown dev:ino=(21:285264), mode=0100600, own=(0,0), nlink=1, rdev=0
  FAKEROOT: insert_or_overwrite unknown stat:
  dev:ino=(21:285264), mode=0100600, own=(0,0), nlink=1, rdev=0
  FAKEROOT: r=1092, received message type=1, message=3
  FAKEROOT: process stat oldstate=dev:ino=(21:285264), mode=0100600, own=(1000,1000), nlink=1, rdev=0
  FAKEROOT: (previously known): fake=dev:ino=(21:285264), mode=0100600, own=(0,0), nlink=1, rdev=0
  AFTER STAT 2
  #?6|kent:tmp$

So

  creat("conftest.file", 0600)            = 3
  close(3)                                = 0
  stat("conftest.file", {st_mode=S_IFREG|0600, st_size=0, ...}) = 0
  clock_nanosleep(CLOCK_REALTIME, 0, {tv_sec=1, tv_nsec=0}, 0x7ffeff1f6a10) = 0
  stat("conftest.file", {st_mode=S_IFREG|0600, st_size=0, ...}) = 0
  lchown("conftest.file", 0, 0)           = -1 EPERM (Operation not permitted)

Fakeroot fails to chown but ignores the error, yet the failed
system call avoids getting the filestamp updated.  Maybe fakeroot
should "simply" perform the task with the original user and group,
which are available, in order to make timestamp updates happen?

  #ifdef LIBFAKEROOT_DEBUGGING
    if (fakeroot_debug) {
      fprintf(stderr, "chown path %s owner %d group %d\n", path, owner, group);
    }
  #endif /* LIBFAKEROOT_DEBUGGING */
  #ifdef LCHOWN_SUPPORT
    /*chown(sym-link) works on the target of the symlink if lchown is
      present and enabled.*/
    r=INT_NEXT_STAT(path, &st);
  #else
    /*chown(sym-link) works on the symlink itself, use lstat: */
    r=INT_NEXT_LSTAT(path, &st);
  #endif

    if(r)
      return r;
    st.st_uid=owner;
    st.st_gid=group;
    INT_SEND_STAT(&st,chown_func);
    if(!dont_try_chown())
      r=next_lchown(path,owner,group);
    else
      r=0;
    if(r&&(errno==EPERM))
      r=0;

Would be a thing to protect against these conflicts.

Well, ok, this is a thing, fakeroot warns against using it when
running GNU autoconf systems, yet i use it to run the package
manager as such (;-}  Yeah, the problem is that the package
footprints are checked, and then owner/group mismatches kill.
Unfortunately the "make" and "make install" rules are run in
a single "build()" step by this package manager .. maybe one of
the very few not so nice decisions.

All i could do otherwise is UID mapping inside this "filesystem
overlay namespace container", but this is a hard thing to get
right .. if at all.  It has been around five years since i
created this environment for package building on this Linux
distro, yours is the first which fails.  I have forgotten why
i did not use UID mapping stuff.  It is also, maybe, that i do
*not* want to be root in the namespace.  One reason why i do not
use a VM, or LXC (or what its name was) or docker or all that.
Simple mostly read-only overlayfs that then hosts

        ip netns exec ${NETNS} \
                /usr/bin/env -i TERM=${TERM} \
                        /usr/bin/unshare --ipc --uts --pid --fork --mount --mount-proc ${kill_child} \
                                ${rooter} /init

where /init in this case effectively is

  ${SUPER} -u ports /usr/bin/nice -n +20 fakeroot pkgmk ""${@}

where $SUPER is doas(1) (or sudo or super that existed around Y2K
and has not seen for long even via Google).

Having said all that, and fakeroot aside, GNU cp(1) should not
exit error if it fails to preserve permissions for a symbolic link

  #?1|kent:tmp$ rm xc
  #?0|kent:tmp$ /bin/cp -a xb xc
  cp: failed to preserve ownership for xc: Operation not supported
  #?1|kent:tmp$ ls -l xa xb xc
  -rw-r----- 1 steffen steffen 0 Sep 16 19:48 xa
  lrwxrwxrwx 1 steffen steffen 2 Sep 17 18:44 xc -> xa
  lrwxrwxrwx 1 steffen steffen 2 Sep 16 19:48 xb -> xa

that way, so some fallback path in GNU coreutils is "bogus".

 --End of <[email-masked]>

--steffen
|
|Der Kragenbaer,                The moon bear,
|der holt sich munter           he cheerfully and one by one
|einen nach dem anderen runter  wa.ks himself off
|(By Robert Gernhardt)