This fixes some unlikely security races,
where our “no-op” chmod undid some other process’s chmod.
Ironically this bug occurred on OpenBSD, our most paranoid target.
This patch also fixes some EOVERFLOW bugs,
along with a performance bug and a CHOWN_CHANGE_TIME_BUG with fchownat.
* lib/chown.c, lib/fchownat.c, lib/lchown.c:
Remove unnecessary inconsistencies.
Include stat-time.h.
(CHOWN_CHANGE_TIME_BUG, CHOWN_FAILS_TO_HONOR_ID_OF_NEGATIVE_ONE)
(CHOWN_MODIFIES_SYMLINK, CHOWN_TRAILING_SLASH_BUG):
Default to 0, and prefer ‘if (...)’ to ‘#ifdef ...’.
(utimensat) [!HAVE_UTIMENSAT]: Default to a no-op.
(rpl_chown, rpl_fchownat, rpl_lchown):
Prefer ‘if (...)’ to ‘#ifdef ...’.
Statically, call the stat-like and chown-like functions just once.
Do not fail if the stat-like function fails with EOVERFLOW,
if existence is all we care about.
Use utimensat to update ctime, instead of a chmod-like function.
* lib/fchownat.c (rpl_fchownat): Defend against OpenBSD’s
CHOWN_CHANGE_TIME_BUG.  This bug in rpl_fchownat was exposed by
yesterday’s fix that caused rpl_fchownat to call fchownat instead
of using the tricky old fork/chdir business.
* m4/chown.m4 (gl_FUNC_CHOWN):
Check for utimensat if the ctime bug is present.
* modules/chown, modules/lchown, modules/fchownat:
(Depends-on): Add stat-time.
---
 ChangeLog                         |  44 ++++++++--
 doc/posix-functions/chown.texi    |   2 +-
 doc/posix-functions/fchownat.texi |   4 +
 doc/posix-functions/lchown.texi   |   5 +-
 lib/chown.c                       | 137 +++++++++++++++---------------
 lib/fchownat.c                    |  69 +++++++++++++--
 lib/lchown.c                      | 131 +++++++++++++++-------------
 m4/chown.m4                       |   3 +-
 modules/chown                     |   3 +-
 modules/fchownat                  |   2 +
 modules/lchown                    |   5 +-
 11 files changed, 256 insertions(+), 149 deletions(-)

diff --git a/ChangeLog b/ChangeLog
index 6f0c34b90d..8e2d1a6eb9 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,33 @@
+2025-09-21  Paul Eggert  <[email protected]>
+
+       fchownat: fix security races and other bugs
+       This fixes some unlikely security races,
+       where our “no-op” chmod undid some other process’s chmod.
+       Ironically this bug occurred on OpenBSD, our most paranoid target.
+       This patch also fixes some EOVERFLOW bugs,
+       along with a performance bug and a CHOWN_CHANGE_TIME_BUG with fchownat.
+       * lib/chown.c, lib/fchownat.c, lib/lchown.c:
+       Remove unnecessary inconsistencies.
+       Include stat-time.h.
+       (CHOWN_CHANGE_TIME_BUG, CHOWN_FAILS_TO_HONOR_ID_OF_NEGATIVE_ONE)
+       (CHOWN_MODIFIES_SYMLINK, CHOWN_TRAILING_SLASH_BUG):
+       Default to 0, and prefer ‘if (...)’ to ‘#ifdef ...’.
+       (utimensat) [!HAVE_UTIMENSAT]: Default to a no-op.
+       (rpl_chown, rpl_fchownat, rpl_lchown):
+       Prefer ‘if (...)’ to ‘#ifdef ...’.
+       Statically, call the stat-like and chown-like functions just once.
+       Do not fail if the stat-like function fails with EOVERFLOW,
+       if existence is all we care about.
+       Use utimensat to update ctime, instead of a chmod-like function.
+       * lib/fchownat.c (rpl_fchownat): Defend against OpenBSD’s
+       CHOWN_CHANGE_TIME_BUG.  This bug in rpl_fchownat was exposed by
+       yesterday’s fix that caused rpl_fchownat to call fchownat instead
+       of using the tricky old fork/chdir business.
+       * m4/chown.m4 (gl_FUNC_CHOWN):
+       Check for utimensat if the ctime bug is present.
+       * modules/chown, modules/lchown, modules/fchownat:
+       (Depends-on): Add stat-time.
+
 2025-09-21  Bruno Haible  <[email protected]>
 
        pthread-once: Fix link error on glibc < 2.34 systems (regr. yesterday).
@@ -9990,7 +10020,7 @@
        * lib/file-has-acl.c (file_has_aclinfo): On FreeBSD, NetBSD >= 10,
        if we don’t follow symlinks the first time, also don’t follow
        them the second time, when it is typically a directory - so it
-       doesn’t matter whether symlinks are followed - but it might not be.
+       doesn’t matter whether symlinks are followed - but it might not be.
 
        file-has-acl: minor refactor of acl_get_link_np fix
        * lib/file-has-acl.c (file_has_aclinfo): Redo to avoid ‘else #endif’.
@@ -16977,7 +17007,7 @@
        tzname: document some limitations
        Unfortunately tzname is a vestigial interface that doesn't work
        <https://data.iana.org/time-zones/theory.html#vestigial>.
-       It's relatively useless in portable code and is planned to be removed
+       It's relatively useless in portable code and is planned to be removed
        from POSIX <https://austingroupbugs.net/view.php?id=1816>.
        Document this better here.
 
@@ -24571,7 +24601,7 @@
        striconveha: pacify gcc -Wcast-align
        * lib/striconveha.c (uniconv_register_autodetect): Rewrite to
        avoid the need to cast from char * to a pointer to a more strictly
-       aligned type.  Use decls after statements to avoid some repetition.
+       aligned type.  Use decls after statements to avoid some repetition.
 
 2023-11-14  Bruno Haible  <[email protected]>
 
@@ -68426,7 +68456,7 @@
 2017-11-28  Benno Schulenberg  <[email protected]>
 
        stat: fix compilation failure on macOS Sierra
-       Reported by Marius Schamschula <[email protected]> in:
+       Reported by Marius Schamschula <[email protected]> in:
        https://savannah.gnu.org/bugs/?52546
        * lib/stat.c: Add missing include of stat-time.h.
 
@@ -69858,7 +69888,7 @@
 
        We take exact tag "v0.2-rc1" for the old format, extract the presumed
        tag "v0.2" from it, then run "git rev-list v0.2..HEAD" to count
-       commits since tha tag.  Fails, because tag "v0.2" does not exist.
+       commits since tha tag.  Fails, because tag "v0.2" does not exist.
 
        * git-version-gen: We could perhaps drop support for versions from
        more than a decade ago.  But tightening the pattern match is easy
@@ -76770,7 +76800,7 @@
 2015-12-17  Paul Eggert  <[email protected]>
 
        intprops: comment fix
-       * lib/intprops.h: Fix comment.  Reported by Pádraig Brady in:
+       * lib/intprops.h: Fix comment.  Reported by Pádraig Brady in:
        http://lists.gnu.org/r/bug-gnulib/2015-12/msg00013.html
 
        intprops-test: work around GCC bug 68971
@@ -98105,7 +98135,7 @@
 2011-07-22  Paul Eggert  <[email protected]>
 
        largefile: new module, replacing large-inode
-       Pádraig Brady suggested this in <http://debbugs.gnu.org/9140#20>.
+       Pádraig Brady suggested this in <http://debbugs.gnu.org/9140#20>.
        * MODULES.html.sh: Add largefile, remove large-inode.
        * modules/largefile, m4/largefile.m4: New files.
        * modules/large-inode, m4/large-inode.m4: Remove.
diff --git a/doc/posix-functions/chown.texi b/doc/posix-functions/chown.texi
index 0e3fde58c3..fb100efa04 100644
--- a/doc/posix-functions/chown.texi
+++ b/doc/posix-functions/chown.texi
@@ -16,7 +16,7 @@ macOS 14, FreeBSD 7.2, AIX 7.3.1, Solaris 9.
 @item
 Some platforms fail to update the change time when at least one
 argument was not @minus{}1, but no ownership changes resulted:
-OpenBSD 7.2.
+OpenBSD 7.7.
 @item
 When passed an argument of @minus{}1, some implementations really set the owner
 user/group id of the file to this value, rather than leaving that id of the
diff --git a/doc/posix-functions/fchownat.texi 
b/doc/posix-functions/fchownat.texi
index 4e6416f940..34c1d724f1 100644
--- a/doc/posix-functions/fchownat.texi
+++ b/doc/posix-functions/fchownat.texi
@@ -26,6 +26,10 @@ Some platforms fail to detect trailing slash on 
non-directories, as in
 @code{fchown(dir,"link-to-file/",uid,gid,flag)}:
 Solaris 9.
 @item
+Some platforms fail to update the change time when at least one
+argument was not @minus{}1, but no ownership changes resulted:
+OpenBSD 7.7.
+@item
 Some platforms mistakenly dereference symlinks when using
 @code{AT_SYMLINK_NOFOLLOW}:
 Linux kernel 2.6.17.
diff --git a/doc/posix-functions/lchown.texi b/doc/posix-functions/lchown.texi
index efe003a6c6..2ce88491b3 100644
--- a/doc/posix-functions/lchown.texi
+++ b/doc/posix-functions/lchown.texi
@@ -23,9 +23,8 @@ Some platforms fail to detect trailing slash on 
non-directories, as in
 FreeBSD 7.2, Solaris 9.
 @item
 Some platforms fail to update the change time when at least one
-argument was not -1, but no ownership changes resulted.  However,
-without @code{lchmod}, the replacement only fixes this for non-symlinks:
-OpenBSD 4.0.
+argument was not @minus{}1, but no ownership changes resulted:
+OpenBSD 7.7.
 @end itemize
 
 Portability problems not fixed by Gnulib:
diff --git a/lib/chown.c b/lib/chown.c
index d2af041e8e..61a0964fe1 100644
--- a/lib/chown.c
+++ b/lib/chown.c
@@ -1,5 +1,4 @@
-/* provide consistent interface to chown for systems that don't interpret
-   an ID of -1 as meaning "don't change the corresponding ID".
+/* A more POSIX-compliant chown
 
    Copyright (C) 1997, 2004-2007, 2009-2025 Free Software Foundation, Inc.
 
@@ -29,13 +28,36 @@
 #include <sys/stat.h>
 
 #include "issymlink.h"
+#include "stat-time.h"
+
+#ifndef CHOWN_CHANGE_TIME_BUG
+# define CHOWN_CHANGE_TIME_BUG 0
+#endif
+#ifndef CHOWN_FAILS_TO_HONOR_ID_OF_NEGATIVE_ONE
+# define CHOWN_FAILS_TO_HONOR_ID_OF_NEGATIVE_ONE 0
+#endif
+#ifndef CHOWN_MODIFIES_SYMLINK
+# define CHOWN_MODIFIES_SYMLINK 0
+#endif
+#ifndef CHOWN_TRAILING_SLASH_BUG
+# define CHOWN_TRAILING_SLASH_BUG 0
+#endif
+
+/* Gnulib target platforms lacking utimensat do not need it,
+   because in practice the bug it works around does not occur.  */
+#if !HAVE_UTIMENSAT
+# undef utimensat
+# define utimensat(fd, file, times, flag) \
+    ((void) (fd), (void) (file), (void) (times), (void) (flag), \
+     0)
+#endif
 
 #if !HAVE_CHOWN
 
 /* Simple stub that always fails with ENOSYS, for mingw.  */
 int
-chown (_GL_UNUSED const char *file, _GL_UNUSED uid_t uid,
-       _GL_UNUSED gid_t gid)
+chown (_GL_UNUSED const char *file, _GL_UNUSED uid_t owner,
+       _GL_UNUSED gid_t group)
 {
   errno = ENOSYS;
   return -1;
@@ -43,54 +65,20 @@ chown (_GL_UNUSED const char *file, _GL_UNUSED uid_t uid,
 
 #else /* HAVE_CHOWN */
 
-/* Below we refer to the system's chown().  */
+/* Below we refer to the system's function.  */
 # undef chown
 
-/* Provide a more-closely POSIX-conforming version of chown on
-   systems with one or both of the following problems:
-   - chown doesn't treat an ID of -1 as meaning
-   "don't change the corresponding ID".
-   - chown doesn't dereference symlinks.  */
+/* Provide a more-closely POSIX-conforming version.  */
 
 int
-rpl_chown (const char *file, uid_t uid, gid_t gid)
+rpl_chown (const char *file, uid_t owner, gid_t group)
 {
-# if (CHOWN_CHANGE_TIME_BUG || CHOWN_FAILS_TO_HONOR_ID_OF_NEGATIVE_ONE \
-      || CHOWN_TRAILING_SLASH_BUG)
-  struct stat st;
-  bool stat_valid = false;
-# endif
-  int result;
-
-# if CHOWN_CHANGE_TIME_BUG /* OpenBSD 7.2 */
-  if (gid != (gid_t) -1 || uid != (uid_t) -1)
-    {
-      if (stat (file, &st))
-        return -1;
-      stat_valid = true;
-    }
-# endif
-
-# if CHOWN_FAILS_TO_HONOR_ID_OF_NEGATIVE_ONE /* some very old platforms */
-  if (gid == (gid_t) -1 || uid == (uid_t) -1)
-    {
-      /* Stat file to get id(s) that should remain unchanged.  */
-      if (!stat_valid && stat (file, &st))
-        return -1;
-      stat_valid = true;
-      if (gid == (gid_t) -1)
-        gid = st.st_gid;
-      if (uid == (uid_t) -1)
-        uid = st.st_uid;
-    }
-# endif
-
-# if CHOWN_MODIFIES_SYMLINK /* some very old platforms */
-  /* The system-supplied chown function does not follow symlinks.
+  /* In some very old platforms, the system-supplied function
+     does not follow symlinks.
      If the file is a symlink, open the file (following symlinks), and
      fchown the resulting descriptor.  Although the open might fail
      due to lack of permissions, it's the best we can easily do.  */
-  if (issymlink (file) > 0)
+  if (CHOWN_MODIFIES_SYMLINK && 0 < issymlink (file))
     {
       int open_flags = O_NONBLOCK | O_NOCTTY | O_CLOEXEC;
       int fd = open (file, O_RDONLY | open_flags);
@@ -103,39 +91,52 @@ rpl_chown (const char *file, uid_t uid, gid_t gid)
               || ((fd = open (file, O_SEARCH | open_flags)) < 0)))
         return fd;
 
-      int r = fchown (fd, uid, gid);
+      int r = fchown (fd, owner, group);
       int err = errno;
       close (fd);
       errno = err;
       return r;
     }
-# endif
 
-# if CHOWN_TRAILING_SLASH_BUG /* macOS 12.5, FreeBSD 7.2, AIX 7.3.1, Solaris 9 
*/
-  if (!stat_valid)
+  struct stat st;
+  gid_t no_gid = -1;
+  uid_t no_uid = -1;
+  bool gid_noop = group == no_gid;
+  bool uid_noop = owner == no_uid;
+  bool change_time_check = CHOWN_CHANGE_TIME_BUG && !(gid_noop & uid_noop);
+  bool negative_one_check = (CHOWN_FAILS_TO_HONOR_ID_OF_NEGATIVE_ONE
+                             && (gid_noop | uid_noop));
+  if (change_time_check | negative_one_check
+      || (CHOWN_TRAILING_SLASH_BUG
+          && file[0] && file[strlen (file) - 1] == '/'))
     {
-      size_t len = strlen (file);
-      if (len && file[len - 1] == '/' && stat (file, &st))
-        return -1;
-    }
-# endif
+      int r = stat (file, &st);
 
-  result = chown (file, uid, gid);
-
-# if CHOWN_CHANGE_TIME_BUG /* OpenBSD 7.2 */
-  if (result == 0 && stat_valid
-      && (uid == st.st_uid || uid == (uid_t) -1)
-      && (gid == st.st_gid || gid == (gid_t) -1))
-    {
-      /* No change in ownership, but at least one argument was not -1,
-         so we are required to update ctime.  Since chown succeeded,
-         we assume that chmod will do likewise.  Fortunately, on all
-         known systems where a 'no-op' chown skips the ctime update, a
-         'no-op' chmod still does the trick.  */
-      result = chmod (file, st.st_mode & (S_IRWXU | S_IRWXG | S_IRWXO
-                                          | S_ISUID | S_ISGID | S_ISVTX));
+      /* EOVERFLOW means the file exists, which is all that the
+         trailing slash check needs.  */
+      if (r < 0 && (change_time_check | negative_one_check
+                    || errno != EOVERFLOW))
+        return r;
     }
-# endif
+
+  gid_t uid = (CHOWN_FAILS_TO_HONOR_ID_OF_NEGATIVE_ONE && uid_noop
+               ? st.st_uid : owner);
+  gid_t gid = (CHOWN_FAILS_TO_HONOR_ID_OF_NEGATIVE_ONE && gid_noop
+               ? st.st_gid : group);
+  int result = chown (file, uid, gid);
+
+  /* If no change in ownership, but at least one argument was not -1,
+     update ctime indirectly via a no-change update to atime and mtime.
+     Do not use UTIME_NOW or UTIME_OMIT as they might run into bugs
+     on some platforms.  Do not communicate any failure to the caller
+     as that would be worse than communicating the ownership change.  */
+  if (result == 0 && change_time_check
+      && (((uid == st.st_uid) | uid_noop)
+          & ((gid == st.st_gid) | gid_noop)))
+    utimensat (AT_FDCWD, file,
+               ((struct timespec[]) { get_stat_atime (&st),
+                                      get_stat_mtime (&st) }),
+               0);
 
   return result;
 }
diff --git a/lib/fchownat.c b/lib/fchownat.c
index 3e15fde1da..cce3a7abd4 100644
--- a/lib/fchownat.c
+++ b/lib/fchownat.c
@@ -31,8 +31,29 @@
 #include <errno.h>
 #include <stdlib.h>
 #include <string.h>
+#include <sys/stat.h>
 
 #include "openat.h"
+#include "stat-time.h"
+
+#ifndef CHOWN_CHANGE_TIME_BUG
+# define CHOWN_CHANGE_TIME_BUG 0
+#endif
+#ifndef CHOWN_TRAILING_SLASH_BUG
+# define CHOWN_TRAILING_SLASH_BUG 0
+#endif
+#ifndef FCHOWNAT_EMPTY_FILENAME_BUG
+# define FCHOWNAT_EMPTY_FILENAME_BUG 0
+#endif
+
+/* Gnulib target platforms lacking utimensat do not need it,
+   because in practice the bug it works around does not occur.  */
+#if !HAVE_UTIMENSAT
+# undef utimensat
+# define utimensat(fd, file, times, flag) \
+    ((void) (fd), (void) (file), (void) (times), (void) (flag), \
+     0)
+#endif
 
 #if !HAVE_FCHOWNAT
 
@@ -88,28 +109,58 @@ local_lchownat (int fd, char const *file, uid_t owner, 
gid_t group);
 int
 rpl_fchownat (int fd, char const *file, uid_t owner, gid_t group, int flag)
 {
+  /* No need to worry about CHOWN_FAILS_TO_HONOR_ID_OF_NEGATIVE_ONE
+     or CHOWN_MODIFIES_SYMLINK, as no known fchownat implementations
+     have these bugs.  */
+
 # if FCHOWNAT_NOFOLLOW_BUG
   if (flag == AT_SYMLINK_NOFOLLOW)
     return local_lchownat (fd, file, owner, group);
 # endif
-# if FCHOWNAT_EMPTY_FILENAME_BUG
-  if (file[0] == '\0')
+
+  if (FCHOWNAT_EMPTY_FILENAME_BUG && file[0] == '\0')
     {
       errno = ENOENT;
       return -1;
     }
-# endif
-# if CHOWN_TRAILING_SLASH_BUG
-  if (file[0] && file[strlen (file) - 1] == '/')
+
+  struct stat st;
+  gid_t no_gid = -1;
+  uid_t no_uid = -1;
+  bool gid_noop = group == no_gid;
+  bool uid_noop = owner == no_uid;
+  bool change_time_check = CHOWN_CHANGE_TIME_BUG && !(gid_noop & uid_noop);
+
+  if (change_time_check
+      || (CHOWN_TRAILING_SLASH_BUG
+          && file[0] && file[strlen (file) - 1] == '/'))
     {
-      struct stat st;
       int r = fstatat (fd, file, &st, 0);
-      if (r < 0 && errno != EOVERFLOW)
+
+      /* EOVERFLOW means the file exists, which is all that the
+         trailing slash check needs.  */
+      if (r < 0 && (change_time_check || errno != EOVERFLOW))
         return r;
+
       flag &= ~AT_SYMLINK_NOFOLLOW;
     }
-# endif
-  return fchownat (fd, file, owner, group, flag);
+
+  int result = fchownat (fd, file, owner, group, flag);
+
+  /* If no change in ownership, but at least one argument was not -1,
+     update ctime indirectly via a no-change update to atime and mtime.
+     Do not use UTIME_NOW or UTIME_OMIT as they might run into bugs
+     on some platforms.  Do not communicate any failure to the caller
+     as that would be worse than communicating the ownership change.  */
+  if (result == 0 && change_time_check
+      && (((owner == st.st_uid) | uid_noop)
+          & ((group == st.st_gid) | gid_noop)))
+    utimensat (fd, file,
+               ((struct timespec[]) { get_stat_atime (&st),
+                                      get_stat_mtime (&st) }),
+               flag);
+
+  return result;
 }
 
 #endif /* HAVE_FCHOWNAT */
diff --git a/lib/lchown.c b/lib/lchown.c
index efcb23fe74..ef92c18c8f 100644
--- a/lib/lchown.c
+++ b/lib/lchown.c
@@ -1,4 +1,4 @@
-/* Provide a stub lchown function for systems that lack it.
+/* A more POSIX-compliant lchown
 
    Copyright (C) 1998-1999, 2002, 2004, 2006-2007, 2009-2025 Free Software
    Foundation, Inc.
@@ -24,11 +24,31 @@
 #include <unistd.h>
 
 #include <errno.h>
+#include <fcntl.h>
 #include <string.h>
 #include <sys/stat.h>
 
 #include "issymlink.h"
 
+#ifndef CHOWN_CHANGE_TIME_BUG
+# define CHOWN_CHANGE_TIME_BUG 0
+#endif
+#ifndef CHOWN_MODIFIES_SYMLINK
+# define CHOWN_MODIFIES_SYMLINK 0
+#endif
+#ifndef CHOWN_TRAILING_SLASH_BUG
+# define CHOWN_TRAILING_SLASH_BUG 0
+#endif
+
+/* Gnulib target platforms lacking utimensat do not need it,
+   because in practice the bug it works around does not occur.  */
+#if !HAVE_UTIMENSAT
+# undef utimensat
+# define utimensat(fd, file, times, flag) \
+    ((void) (fd), (void) (file), (void) (times), (void) (flag), \
+     0)
+#endif
+
 #if !HAVE_LCHOWN
 
 /* If the system chown does not follow symlinks, we don't want it
@@ -43,18 +63,17 @@
    symlinks, then just call chown.  */
 
 int
-lchown (const char *file, uid_t uid, gid_t gid)
+lchown (_GL_UNUSED char const *file, _GL_UNUSED uid_t owner,
+        _GL_UNUSED gid_t group)
 {
 # if HAVE_CHOWN
-#  if ! CHOWN_MODIFIES_SYMLINK
-  if (issymlink (file) > 0)
+  if (!CHOWN_MODIFIES_SYMLINK && 0 < issymlink (file))
     {
       errno = EOPNOTSUPP;
       return -1;
     }
-#  endif
 
-  return chown (file, uid, gid);
+  return chown (file, owner, group);
 
 # else /* !HAVE_CHOWN */
   errno = ENOSYS;
@@ -64,65 +83,63 @@ lchown (const char *file, uid_t uid, gid_t gid)
 
 #else /* HAVE_LCHOWN */
 
+/* Below we refer to the system's function.  */
 # undef lchown
 
-/* Work around trailing slash bugs in lchown.  */
+/* Provide a more-closely POSIX-conforming version.  */
+
 int
-rpl_lchown (const char *file, uid_t uid, gid_t gid)
+rpl_lchown (const char *file, uid_t owner, gid_t group)
 {
-  bool stat_valid = false;
-  int result;
-
-# if CHOWN_CHANGE_TIME_BUG
   struct stat st;
-
-  if (gid != (gid_t) -1 || uid != (uid_t) -1)
+  gid_t no_gid = -1;
+  uid_t no_uid = -1;
+  bool gid_noop = group == no_gid;
+  bool uid_noop = owner == no_uid;
+  bool change_time_check = CHOWN_CHANGE_TIME_BUG && !(gid_noop & uid_noop);
+
+  if (change_time_check
+      || (CHOWN_TRAILING_SLASH_BUG
+          && file[0] && file[strlen (file) - 1] == '/'))
     {
-      /* Prefer readlink to lstat+S_ISLNK, to avoid EOVERFLOW issues
-         in the common case where FILE is a non-symlink.  */
-      int ret = issymlink (file);
-      if (ret < 0)
-        return -1;
-      if (ret == 0)
-        /* FILE is not a symlink.  */
-        return chown (file, uid, gid);
-
-      /* Later code can use the status, so get it if possible.  */
-      ret = lstat (file, &st);
-      if (ret < 0)
-        return -1;
-      /* An easy check: did FILE change from a symlink to a non-symlink?  */
-      if (!S_ISLNK (st.st_mode))
-        return chown (file, uid, gid);
-
-      stat_valid = true;
+      bool file_is_symlink = false;
+      int r = lstat (file, &st);
+      if (0 <= r)
+        file_is_symlink = !!S_ISLNK (st.st_mode);
+      else if (errno != EOVERFLOW)
+        return r;
+      else
+        {
+          int s = issymlink (file);
+          if (s < 0)
+            return s;
+          if (0 < s)
+            {
+              errno = EOVERFLOW;
+              return -1;
+            }
+          /* FILE exists and is not a symbolic link; ST is unset.
+             Rely on Gnulib chown to work around platform chown bugs.  */
+        }
+
+      if (!file_is_symlink)
+        return chown (file, owner, group);
     }
-# endif
 
-# if CHOWN_TRAILING_SLASH_BUG
-  if (!stat_valid)
-    {
-      size_t len = strlen (file);
-      if (len && file[len - 1] == '/')
-        return chown (file, uid, gid);
-    }
-# endif
-
-  result = lchown (file, uid, gid);
-
-# if CHOWN_CHANGE_TIME_BUG && HAVE_LCHMOD
-  if (result == 0 && stat_valid
-      && (uid == st.st_uid || uid == (uid_t) -1)
-      && (gid == st.st_gid || gid == (gid_t) -1))
-    {
-      /* No change in ownership, but at least one argument was not -1,
-         so we are required to update ctime.  Since lchown succeeded,
-         we assume that lchmod will do likewise.  But if the system
-         lacks lchmod and lutimes, we are out of luck.  Oh well.  */
-      result = lchmod (file, st.st_mode & (S_IRWXU | S_IRWXG | S_IRWXO
-                                           | S_ISUID | S_ISGID | S_ISVTX));
-    }
-# endif
+  int result = lchown (file, owner, group);
+
+  /* If no change in ownership, but at least one argument was not -1,
+     update ctime indirectly via a no-change update to atime and mtime.
+     Do not use UTIME_NOW or UTIME_OMIT as they might run into bugs
+     on some platforms.  Do not communicate any failure to the caller
+     as that would be worse than communicating the ownership change.  */
+  if (result == 0 && change_time_check
+      && (((owner == st.st_uid) | uid_noop)
+          & ((group == st.st_gid) | gid_noop)))
+    utimensat (AT_FDCWD, file,
+               ((struct timespec[]) { get_stat_atime (&st),
+                                      get_stat_mtime (&st) }),
+               AT_SYMLINK_NOFOLLOW);
 
   return result;
 }
diff --git a/m4/chown.m4 b/m4/chown.m4
index 2c06cfdd94..8105cfd3ae 100644
--- a/m4/chown.m4
+++ b/m4/chown.m4
@@ -1,5 +1,5 @@
 # chown.m4
-# serial 36
+# serial 37
 dnl Copyright (C) 1997-2001, 2003-2005, 2007, 2009-2025 Free Software
 dnl Foundation, Inc.
 dnl This file is free software; the Free Software Foundation
@@ -163,6 +163,7 @@ AC_DEFUN_ONCE([gl_FUNC_CHOWN],
     case "$gl_cv_func_chown_ctime_works" in
       *yes) ;;
       *)
+        gl_CHECK_FUNCS_ANDROID([utimensat], [[#include <sys/stat.h>]])
         AC_DEFINE([CHOWN_CHANGE_TIME_BUG], [1], [Define to 1 if chown fails
           to change ctime when at least one argument was not -1.])
         REPLACE_CHOWN=1
diff --git a/modules/chown b/modules/chown
index b1aedde85c..37b3b5eaf2 100644
--- a/modules/chown
+++ b/modules/chown
@@ -8,11 +8,12 @@ m4/chown.m4
 
 Depends-on:
 unistd-h
+bool            [test $HAVE_CHOWN = 0 || test $REPLACE_CHOWN = 1]
 fstat           [test $HAVE_CHOWN = 0 || test $REPLACE_CHOWN = 1]
 issymlink       [test $HAVE_CHOWN = 0 || test $REPLACE_CHOWN = 1]
 open            [test $HAVE_CHOWN = 0 || test $REPLACE_CHOWN = 1]
 stat            [test $HAVE_CHOWN = 0 || test $REPLACE_CHOWN = 1]
-bool            [test $HAVE_CHOWN = 0 || test $REPLACE_CHOWN = 1]
+stat-time       [test $HAVE_CHOWN = 0 || test $REPLACE_CHOWN = 1]
 sys_stat-h      [test $HAVE_CHOWN = 0 || test $REPLACE_CHOWN = 1]
 
 configure.ac:
diff --git a/modules/fchownat b/modules/fchownat
index e1ffb97282..31e722c89c 100644
--- a/modules/fchownat
+++ b/modules/fchownat
@@ -10,6 +10,7 @@ Depends-on:
 unistd-h
 extensions
 at-internal     [test $HAVE_FCHOWNAT = 0 || test $REPLACE_FCHOWNAT = 1]
+bool            [test $HAVE_FCHOWNAT = 0 || test $REPLACE_FCHOWNAT = 1]
 errno-h         [test $HAVE_FCHOWNAT = 0 || test $REPLACE_FCHOWNAT = 1]
 extern-inline   [test $HAVE_FCHOWNAT = 0 || test $REPLACE_FCHOWNAT = 1]
 fchdir          [test $HAVE_FCHOWNAT = 0 || test $REPLACE_FCHOWNAT = 1]
@@ -19,6 +20,7 @@ lchown          [test $HAVE_FCHOWNAT = 0 || test 
$REPLACE_FCHOWNAT = 1]
 openat-die      [test $HAVE_FCHOWNAT = 0 || test $REPLACE_FCHOWNAT = 1]
 openat-h        [test $HAVE_FCHOWNAT = 0 || test $REPLACE_FCHOWNAT = 1]
 save-cwd        [test $HAVE_FCHOWNAT = 0 || test $REPLACE_FCHOWNAT = 1]
+stat-time       [test $HAVE_FCHOWNAT = 0 || test $REPLACE_FCHOWNAT = 1]
 fstatat         [test $REPLACE_FCHOWNAT = 1]
 
 configure.ac:
diff --git a/modules/lchown b/modules/lchown
index 94ba7a9cdd..225e2876d3 100644
--- a/modules/lchown
+++ b/modules/lchown
@@ -7,10 +7,11 @@ m4/lchown.m4
 
 Depends-on:
 unistd-h
-issymlink       [test $HAVE_LCHOWN = 0 || test $REPLACE_LCHOWN = 1]
+bool            [test $HAVE_LCHOWN = 0 || test $REPLACE_LCHOWN = 1]
 chown           [test $HAVE_LCHOWN = 0 || test $REPLACE_LCHOWN = 1]
 errno-h         [test $HAVE_LCHOWN = 0 || test $REPLACE_LCHOWN = 1]
-bool            [test $HAVE_LCHOWN = 0 || test $REPLACE_LCHOWN = 1]
+issymlink       [test $HAVE_LCHOWN = 0 || test $REPLACE_LCHOWN = 1]
+stat-time       [test $HAVE_LCHOWN = 0 || test $REPLACE_LCHOWN = 1]
 sys_stat-h      [test $HAVE_LCHOWN = 0 || test $REPLACE_LCHOWN = 1]
 lstat           [test $REPLACE_LCHOWN = 1]
 
-- 
2.48.1


Reply via email to