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

From: Steffen Nurpmeso <[email-masked]>
Date: Tue, 16 Sep 2025 02:40:07 +0200

Hello.

Sorry for the late reply.

Paul Eggert wrote in
 <[email-masked]>:
 |On 2025-09-13 10:36, Steffen Nurpmeso wrote:
 |> Paul Eggert wrote in
 |>|that cp fails because fchmodat2 fails. So it sounds like we have been
 |>|barking up the wrong tree by worrying about the chown-related syscalls,
 |>|and that we should instead worry about chmod-related syscalls.
 |> 
 |> Seems like that.
 |
 |On further looking at your trace in 
 |<https://debbugs.gnu.org/cgi/bugreport.cgi?bug=79433#23> it seems that I 
 |was mistaken. Even though the fchmodat2 syscall fails with ENOSYS, it 

Please stop right here.
I followed your advice and rebuilt coreutils 9.7.  I cannot
reproduce the error, strace(1)d flows change due to fchmodat2().
I give the logs below.

  $ grep chown /usr/include/asm/unistd_64.h
  #define __NR_chown 92
  #define __NR_fchown 93
  #define __NR_lchown 94
  #define __NR_fchownat 260

  $ grep fchmod /usr/include/asm/unistd_64.h
  #define __NR_fchmod 91
  #define __NR_fchmodat 268

  $ ll /bin/cp
  -rwxr-xr-x 1 root root 126240 Apr 29 11:59 /bin/cp*

  $ ll src/cp
  -rwxr-x--- 1 steffen steffen 480888 Sep 16 01:18 src/cp*

On my box, since April, quite some things changed (CRUX is rolling
release, except for certain core ports which only get bugfixes).
I cannot reconstruct exact software environment from April.

  $ strings /bin/cp | grep -i gcc:
  GCC: (CRUX-x86_64-multilib) 14.2.0

  $ strings src/cp | grep -i gcc:
  GCC: (CRUX-x86_64-multilib) 14.3.0

That.  glibc 2.40 is here since last Octobre (20th) i would
think, it was updated three or four times (CVEs .. at least).
Kernel is 6.1.* ever since it was declared LTS.

 |appears that the glibc fchmodat library function then calls openat with 
 |O_NOFOLLOW|O_PATH and then calls fstatat+AT_EMPTY_PATH, determines that 
 |"c" is a symlink, and then returns -1 with errno=ENOTSUP.
 |
 |But then I'm lost. If b is a symlink, I don't see why 'cp -a b c' would 
 |ever call fchmodat. The coreutils 9.7 source code says that fopr 
 |symlinks copy_internal should call symlinkat via force_symlinkat 
 |(copy.c:3117), then fchownat via lchownat (copy.c:3152), then utimensat 
 |(copy.c:3227), but it should never call fchmodat.
 |
 |Could you investigate why coreutils 9.7 cp is not behaving the way the 
 |source code says it should? You can use a debugger, or insert print 
 |statements, to figure out why in this particular case it is going awry.
 |
 |Lets get the chmod issue figured out first. We can worry about the chown 
 |issue later; one thing at a time.

So i instrumented the code paths first, which was stupid, since it
just works if compiled now.  I do have the /bin/cp from April. 
Here is the diff.

  --- _bin_cp.log	2025-09-16 01:22:35.366585945 +0200
  +++ _src_cp.log	2025-09-16 01:23:14.693251872 +0200
  @@ -1,40 +1,40 @@
  -execve("/bin/cp", ["/bin/cp", "-a", "xb", "xc"], 0x7ffee5e2ffb8 /* 94 vars */) = 0
  -brk(NULL)                               = 0x55d3bf6d7000
  -mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fbbd5a6c000
  +execve("src/cp", ["src/cp", "-a", "xb", "xc"], 0x7ffcd92e2298 /* 94 vars */) = 0
  +brk(NULL)                               = 0x55b6475d1000
  +mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f3595bd8000
   access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
   ...
   openat(AT_FDCWD, "/usr/lib/libc.so.6", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
  @@ -43,52 +43,106 @@ read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0
  ...
   prlimit64(0, RLIMIT_STACK, NULL, {rlim_cur=8192*1024, rlim_max=RLIM64_INFINITY}) = 0
  -getrandom("\x03\x0b\xe6\x00\xb2\xbe\xbd\x51", 8, GRND_NONBLOCK) = 8
  -brk(NULL)                               = 0x55d3bf6d7000
  -brk(0x55d3bf6f8000)                     = 0x55d3bf6f8000
  +getrandom("\x4f\x4b\xf5\x75\x90\xf4\x18\xef", 8, GRND_NONBLOCK) = 8
  +brk(NULL)                               = 0x55b6475d1000
  +brk(0x55b6475f2000)                     = 0x55b6475f2000
   openat(AT_FDCWD, "/usr/lib/locale/locale-archive", O_RDONLY|O_CLOEXEC) = 3
   fstat(3, {st_mode=S_IFREG|0644, st_size=3061520, ...}) = 0
  -mmap(NULL, 3061520, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7fbbd5400000
  +mmap(NULL, 3061520, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f3595600000
   close(3)                                = 0
   geteuid()                               = 1000
  +write(2, "PRE DO_COPY\n", 12PRE DO_COPY
  +)           = 12
  +write(2, "DO_COPY\n", 8DO_COPY
  +)                = 8
   openat(AT_FDCWD, "xc", O_RDONLY|O_PATH|O_DIRECTORY) = -1 ENOTDIR (Not a directory)
  +write(2, "WORK2 xb xc\n", 12WORK2 xb xc
  +)           = 12
  +write(2, "COPY_INTERNAL xb -> xc\n", 23COPY_INTERNAL xb -> xc
  +) = 23
   newfstatat(AT_FDCWD, "xb", {st_mode=S_IFLNK|0777, st_size=2, ...}, AT_SYMLINK_NOFOLLOW) = 0
  +write(2, "      10\n", 9      10
  +)               = 9
   newfstatat(AT_FDCWD, "xc", {st_mode=S_IFLNK|0777, st_size=2, ...}, AT_SYMLINK_NOFOLLOW) = 0
   unlinkat(AT_FDCWD, "xc", 0)             = 0
   readlink("xb", "xa", 3)                 = 2
   symlinkat("xa", AT_FDCWD, "xc")         = 0

==DIVERT

  -newfstatat(AT_FDCWD, "xc", {st_mode=S_IFLNK|0777, st_size=2, ...}, AT_SYMLINK_NOFOLLOW) = 0
  -lchown("xc", 1000, 1000)                = 0
  -fchmodat2(AT_FDCWD, "xc", 0777, AT_SYMLINK_NOFOLLOW) = -1 ENOSYS (Function not implemented)
  -openat(AT_FDCWD, "xc", O_RDONLY|O_NOFOLLOW|O_CLOEXEC|O_PATH) = 3
  -newfstatat(3, "", {st_mode=S_IFLNK|0777, st_size=2, ...}, AT_EMPTY_PATH) = 0
  -close(3)                                = 0

Like you say -- sysdeps/unix/sysv/linux/fchmodat.c went over
fchmodat_fallback().  How did it get here at all is your question;
well, /bin/cp is stripped, CRUX strips stuff.

  -fcntl(1, F_GETFL)                       = 0x2 (flags O_RDWR)
  -write(2, "cp: ", 4cp: )                     = 4
  -write(2, "failed to preserve ownership for"..., 35failed to preserve ownership for xc) = 35
  -write(2, ": Operation not supported", 25: Operation not supported) = 25
  -write(2, "\n", 1
  -)                       = 1
  +write(2, "      314\n", 10      314
  +)             = 10
...
  +write(2, "      316\n", 10      316
  +)             = 10
  +fchownat(AT_FDCWD, "xc", 1000, 1000, AT_SYMLINK_NOFOLLOW) = 0
  +write(2, "      320\n", 10      320
  +)             = 10
..
  +utimensat(AT_FDCWD, "xc", [{tv_sec=1757977042, tv_nsec=253998375} /* 2025-09-16T00:57:22.253998375+0200 */, {tv_sec=1757977033, tv_nsec=957331866} /* 2025-09-16T00:57:13.957331866+0200 */], AT_SYMLINK_NOFOLLOW) = 0
  +llistxattr("xb", NULL, 0)               = 0
  +llistxattr("xb", 0x7ffdb169b050, 0)     = 0
  +write(2, "do_copy returns 1\n", 18do_copy returns 1
  +)     = 18
   lseek(0, 0, SEEK_CUR)                   = -1 ESPIPE (Illegal seek)
   close(0)                                = 0
   close(1)                                = 0
   close(2)                                = 0
  -exit_group(1)                           = ?
  -+++ exited with 1 +++
  +exit_group(0)                           = ?
  ++++ exited with 0 +++

I do have that /bin/cp.
Btw i note that if i strip src/cp it is much smaller than /bin/cp,
but, different compiler etc.
Well, that will be hard, to get more out of that.  I do not use
debuggers.  Well, i guess i could install gdb and set breakpoint
around symlinkat(), then step and try to look around.

 --End of <[email-masked]>

How likely is a miscompilation of gcc 4.2.0?

--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)