This does not add support for AT_EMPTY_PATH on platforms lacking it.
It merely fixes bugs in handling AT_EMPTY_PATH on platforms
that already have AT_EMPTY_PATH.  This is complicated by
the fact that glibc may define AT_EMPTY_PATH even though the
underlying kernel lacks support for it, and by the fact that
in recent kernels, AT_EMPTY_PATH allows the file name to be
a null pointer.
* lib/fchmodat.c (AT_EMPTY_PATH): Default to 0.
(rpl_fchmodat): Do not mishandle AT_EMPTY_PATH if specified
and if the underlying library+kernel supports it.
---
 ChangeLog      | 12 ++++++++++++
 lib/fchmodat.c | 48 ++++++++++++++++++++++++++++++++++--------------
 2 files changed, 46 insertions(+), 14 deletions(-)

diff --git a/ChangeLog b/ChangeLog
index 20459c0447..d78d0d1102 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,5 +1,17 @@
 2026-06-16  Paul Eggert  <[email protected]>
 
+       fchmodat: don’t mishandle AT_EMPTY_PATH
+       This does not add support for AT_EMPTY_PATH on platforms lacking it.
+       It merely fixes bugs in handling AT_EMPTY_PATH on platforms
+       that already have AT_EMPTY_PATH.  This is complicated by
+       the fact that glibc may define AT_EMPTY_PATH even though the
+       underlying kernel lacks support for it, and by the fact that
+       in recent kernels, AT_EMPTY_PATH allows the file name to be
+       a null pointer.
+       * lib/fchmodat.c (AT_EMPTY_PATH): Default to 0.
+       (rpl_fchmodat): Do not mishandle AT_EMPTY_PATH if specified
+       and if the underlying library+kernel supports it.
+
        fstatat etc.: fix wrong machine code for calls
        Without this fix, compilers can generate incorrect machine code
        for functions that call to fstatat-like functions, as the
diff --git a/lib/fchmodat.c b/lib/fchmodat.c
index 9ac951ce68..c41768935b 100644
--- a/lib/fchmodat.c
+++ b/lib/fchmodat.c
@@ -48,18 +48,32 @@ orig_fchmodat (int dir, char const *file, mode_t mode, int 
flags)
 
 #include "issymlinkat.h"
 
+#ifndef AT_EMPTY_PATH
+# define AT_EMPTY_PATH 0
+#endif
+
 /* Invoke chmod or lchmod on FILE, using mode MODE, in the directory
    open on descriptor FD.  If possible, do it without changing the
    working directory.  Otherwise, resort to using save_cwd/fchdir,
    then (chmod|lchmod)/restore_cwd.  If either the save_cwd or the
    restore_cwd fails, then give a diagnostic and exit nonzero.
-   Note that an attempt to use a FLAG value of AT_SYMLINK_NOFOLLOW
-   on a system without lchmod support causes this function to fail.  */
+   Fail if FLAGS contains AT_SYMLINK_NOFOLLOW on a system lacking
+   lchmod support.  */
 
 #if HAVE_FCHMODAT
 int
 fchmodat (int dir, char const *file, mode_t mode, int flags)
 {
+  if (file && *file)
+    flags &= ~AT_EMPTY_PATH;
+  else if (! (flags & AT_EMPTY_PATH))
+    {
+      errno = ENOENT;
+      return -1;
+    }
+  else if (! (flags & AT_SYMLINK_NOFOLLOW))
+    return fchmod (dir, mode);
+
 # if HAVE_NEARLY_WORKING_FCHMODAT
   /* Correct the trailing slash handling.  */
   size_t len = strlen (file);
@@ -77,17 +91,21 @@ fchmodat (int dir, char const *file, mode_t mode, int flags)
 # endif
 
 # if NEED_FCHMODAT_NONSYMLINK_FIX
-  if (flags == AT_SYMLINK_NOFOLLOW)
+  if ((flags & ~AT_EMPTY_PATH) == AT_SYMLINK_NOFOLLOW)
     {
 #  if HAVE_READLINKAT
 #   ifdef O_PATH
-      /* Open a file descriptor with O_NOFOLLOW, to make sure we don't
-         follow symbolic links, if /proc is mounted.  O_PATH is used to
-         avoid a failure if the file is not readable.
-         Cf. <https://sourceware.org/PR14578>  */
-      int fd = openat (dir, file, O_PATH | O_NOFOLLOW | O_CLOEXEC);
-      if (fd < 0)
-        return fd;
+      int fd;
+      if (flags & AT_EMPTY_PATH)
+        fd = dir;
+      else
+        {
+          /* Open with O_NOFOLLOW, so we don't follow symlinks.
+             See <https://sourceware.org/PR14578>.  */
+          fd = openat (dir, file, O_PATH | O_NOFOLLOW | O_CLOEXEC);
+          if (fd < 0)
+            return fd;
+        }
 
       int err;
       {
@@ -105,7 +123,8 @@ fchmodat (int dir, char const *file, mode_t mode, int flags)
           err = errno == ENOENT ? -1 : errno;
       }
 
-      close (fd);
+      if (! (flags & AT_EMPTY_PATH))
+        close (fd);
 
       errno = err;
       if (0 <= err)
@@ -114,15 +133,16 @@ fchmodat (int dir, char const *file, mode_t mode, int 
flags)
 
       /* O_PATH + /proc is not supported.  */
 
-      if (issymlinkat (dir, file) > 0)
+      if (issymlinkat (dir, file ? file : "") > 0)
         {
           errno = EOPNOTSUPP;
           return -1;
         }
 #  endif
 
-      /* Fall back on orig_fchmodat with no flags, despite a possible race.  */
-      flags = 0;
+      /* Fall back on orig_fchmodat without AT_SYMLINK_NOFOLLOW,
+         despite a possible race.  */
+      flags &= ~AT_SYMLINK_NOFOLLOW;
     }
 # endif
 
-- 
2.54.0


Reply via email to