[PATCH] fstatat: support NULL if AT_EMPTY_PATH

Paul Eggert <[email protected]>
Newsgroups gmane.comp.lib.gnulib.bugs
Message-ID <[email protected]>
On platforms that define AT_EMPTY_PATH, allow a null pointer to be
passed as a file name.  This is GNU behavior as of glibc 2.41 +
Linux kernel 6.11, and can be supported on older kernels, older
glibcs, and on recent FreeBSD and z/OS.
* lib/fstatat.c (rpl_fstatat): Support AT_EMPTY_PATH,
if it exists, on null pointers.
* m4/fstatat.m4 (gl_FUNC_FSTATAT): Replace fstatat if
AT_EMPTY_PATH is defined but does not work on null pointers.
Also, define HAVE_WORKING_FSTATAT_ZERO_FLAG, if it is discovered,
regardless of whether replacing fstatat.  This refactoring makes
config.h a bit less confusing.
* tests/test-fstatat.c (main): Test AT_EMPTY_PATH if defined.
---
 ChangeLog                        | 14 ++++++++++
 doc/glibc-functions/statx.texi   |  2 +-
 doc/posix-functions/fstatat.texi | 14 ++++++----
 lib/fstatat.c                    | 17 ++++++++---
 m4/fstatat.m4                    | 48 ++++++++++++++++++++++++++------
 tests/test-fstatat.c             | 12 ++++++++
 6 files changed, 88 insertions(+), 19 deletions(-)

diff --git a/ChangeLog b/ChangeLog
index cc4ca63526..8b4a239eaf 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,5 +1,19 @@
 2026-06-18  Paul Eggert  <[email protected]>
 
+	fstatat: support NULL if AT_EMPTY_PATH
+	On platforms that define AT_EMPTY_PATH, allow a null pointer to be
+	passed as a file name.  This is GNU behavior as of glibc 2.41 +
+	Linux kernel 6.11, and can be supported on older kernels, older
+	glibcs, and on recent FreeBSD and z/OS.
+	* lib/fstatat.c (rpl_fstatat): Support AT_EMPTY_PATH,
+	if it exists, on null pointers.
+	* m4/fstatat.m4 (gl_FUNC_FSTATAT): Replace fstatat if
+	AT_EMPTY_PATH is defined but does not work on null pointers.
+	Also, define HAVE_WORKING_FSTATAT_ZERO_FLAG, if it is discovered,
+	regardless of whether replacing fstatat.  This refactoring makes
+	config.h a bit less confusing.
+	* tests/test-fstatat.c (main): Test AT_EMPTY_PATH if defined.
+
 	fchmodat: fix AT_FDCWD + AT_EMPTY_PATH
 	* lib/fchmodat.c (fchmodat): When following symlinks and with
 	AT_FDCWD and an empty filename, don’t use fchmod as it will fail.
diff --git a/doc/glibc-functions/statx.texi b/doc/glibc-functions/statx.texi
index d57b488dd2..30c2a5cecd 100644
--- a/doc/glibc-functions/statx.texi
+++ b/doc/glibc-functions/statx.texi
@@ -18,7 +18,7 @@ statx
 @item
 When @code{AT_EMPTY_PATH} is used,
 this function does not allow the file name to be a null pointer:
-Linux kernel 6.10.
+glibc 2.40, Linux kernel 6.10.
 @item
 There is an incompatible function of the same name on some platforms:
 AIX 5.2 or newer.
diff --git a/doc/posix-functions/fstatat.texi b/doc/posix-functions/fstatat.texi
index 1f7ab3124b..45331beb51 100644
--- a/doc/posix-functions/fstatat.texi
+++ b/doc/posix-functions/fstatat.texi
@@ -13,9 +13,15 @@ fstatat
 This function is missing on some platforms:
 glibc 2.3.6, Mac OS X 10.5, FreeBSD 6.0, NetBSD 5.0, OpenBSD 3.8, Minix 3.1.8,
 AIX 5.1, HP-UX 11, Cygwin 1.5.x, mingw, MSVC 14.
-But the replacement function is not safe to be used in libraries and is not
+But when the function is missing, its Gnulib substitute
+is not safe to be used in libraries and is not
 thread-safe.
 @item
+On platforms with @code{AT_EMPTY_PATH},
+this function does not work when @code{AT_EMPTY_PATH}
+is used with a null pointer file name:
+glibc 2.40, Linux kernel 6.10, Cygwin 3.6, FreeBSD 15.1.
+@item
 On platforms where @code{off_t} is a 32-bit type, @code{fstatat} may
 not correctly report the size of files or block devices larger than 2
 GB@.  @xref{Large File Support}.
@@ -40,11 +46,7 @@ fstatat
 @item
 This function does not fail when the second argument is an empty string
 on some platforms, even when @code{AT_EMPTY_PATH} is not used:
-glibc 2.7, Linux 2.6.38.
-@item
-When @code{AT_EMPTY_PATH} is used,
-this function does not allow the file name to be a null pointer:
-Linux kernel 6.10, Cygwin 3.6, FreeBSD 15.1.
+glibc 2.7, Linux kernel 2.6.38.
 @item
 This function sets @code{st_ino} only to the low-order 32 bits of
 the inode number of a socket or pipe, which thus can disagree
diff --git a/lib/fstatat.c b/lib/fstatat.c
index 8d26e44501..6b0c564a21 100644
--- a/lib/fstatat.c
+++ b/lib/fstatat.c
@@ -46,6 +46,10 @@ orig_fstatat (int fd, char const *filename, struct stat *buf, int flags)
 #include <stdlib.h>
 #include <string.h>
 
+#ifndef AT_EMPTY_PATH
+# define AT_EMPTY_PATH 0
+#endif
+
 #if HAVE_FSTATAT && HAVE_WORKING_FSTATAT_ZERO_FLAG
 
 # ifndef LSTAT_FOLLOWS_SLASHED_SYMLINK
@@ -68,14 +72,19 @@ normal_fstatat (int fd, char const *file, struct stat *st, int flag)
    Work around this bug if FSTATAT_AT_FDCWD_0_BROKEN is nonzero.  */
 
 int
-rpl_fstatat (int fd, char const *file, struct stat *st, int flag)
+rpl_fstatat (int fd, char const *file, struct stat *st, int flags)
 {
-  int result = normal_fstatat (fd, file, st, flag);
+  /* Implement Linux kernel 6.11+ behavior on platforms that have
+     AT_EMPTY_PATH but do not support it on null pointers.  */
+  if (flags & AT_EMPTY_PATH && !file)
+    file = "";
+
+  int result = normal_fstatat (fd, file, st, flags);
   if (LSTAT_FOLLOWS_SLASHED_SYMLINK || result != 0)
     return result;
 
   size_t len = strlen (file);
-  if (flag & AT_SYMLINK_NOFOLLOW)
+  if (flags & AT_SYMLINK_NOFOLLOW)
     {
       /* Fix lstat behavior.  */
       if (file[len - 1] != '/' || S_ISDIR (st->st_mode))
@@ -85,7 +94,7 @@ rpl_fstatat (int fd, char const *file, struct stat *st, int flag)
           errno = ENOTDIR;
           return -1;
         }
-      result = normal_fstatat (fd, file, st, flag & ~AT_SYMLINK_NOFOLLOW);
+      result = normal_fstatat (fd, file, st, flags & ~AT_SYMLINK_NOFOLLOW);
     }
   /* Fix stat behavior.  */
   if (result == 0 && !S_ISDIR (st->st_mode) && file[len - 1] == '/')
diff --git a/m4/fstatat.m4 b/m4/fstatat.m4
index d53e8d9196..ceeb211d34 100644
--- a/m4/fstatat.m4
+++ b/m4/fstatat.m4
@@ -1,5 +1,5 @@
 # fstatat.m4
-# serial 5
+# serial 6
 dnl Copyright (C) 2004-2026 Free Software Foundation, Inc.
 dnl This file is free software; the Free Software Foundation
 dnl gives unlimited permission to copy and/or distribute it,
@@ -45,6 +45,11 @@ AC_DEFUN([gl_FUNC_FSTATAT]
           esac
          ])
       ])
+    AS_CASE([$gl_cv_func_fstatat_zero_flag],
+      [*yes],
+        [AC_DEFINE([HAVE_WORKING_FSTATAT_ZERO_FLAG], [1],
+           [Define to 1 if fstatat (..., 0) works.
+            For example, it does not work in AIX 7.1.])])
 
     case $gl_cv_func_fstatat_zero_flag+$gl_cv_func_lstat_dereferences_slashed_symlink in
     *yes+*yes) ;;
@@ -56,12 +61,39 @@ AC_DEFUN([gl_FUNC_FSTATAT]
         REPLACE_FSTATAT=1 ;;
     esac
 
-    case $REPLACE_FSTATAT,$gl_cv_func_fstatat_zero_flag in
-      1,*yes)
-         AC_DEFINE([HAVE_WORKING_FSTATAT_ZERO_FLAG], [1],
-           [Define to 1 if fstatat (..., 0) works.
-            For example, it does not work in AIX 7.1.])
-         ;;
-    esac
+    dnl Check for the AT_EMPTY_PATH compatibility issue with null pointers
+    dnl only if not already replacing fstatat.
+    dnl There is no need to AC_DEFINE anything here, as the Gnulib
+    dnl replacement works around the compatibility bug even if the bug
+    dnl is not present, and it is not worth the trouble to tune this.
+    AS_CASE([$REPLACE_FSTATAT],
+      [0],
+        [AC_CACHE_CHECK([whether fstatat+AT_EMPTY_PATH allows null file],
+           [gl_cv_func_fstatat_null_file],
+           [gl_saved_CFLAGS=$CFLAGS
+            CFLAGS="$CFLAGS -Wno-nonnull"
+            AC_RUN_IFELSE(
+              [AC_LANG_PROGRAM(
+                 [[#include <stddef.h>
+                   #include <fcntl.h>
+                   #include <sys/stat.h>
+                   #ifndef AT_EMPTY_PATH
+                    #define AT_EMPTY_PATH 0
+                   #endif
+                   #if __GLIBC__ && ! (2 < __GLIBC__ + (41 <= __GLIBC_MINOR__))
+                    #error "glibc 2.40 and earlier can fail with null file"
+                   #endif
+                 ]],
+                 [[struct stat st;
+                   return
+                     (AT_EMPTY_PATH
+                      && fstatat (AT_FDCWD, NULL, &st, AT_EMPTY_PATH) < 0);
+                 ]])],
+              [gl_cv_func_fstatat_null_file=yes],
+              [gl_cv_func_fstatat_null_file=no],
+              [gl_cv_func_fstatat_null_file="guessing no"])])
+         AS_CASE([$gl_cv_func_fstatat_null_file],
+           [*no],
+             [REPLACE_FSTATAT=1])])
   fi
 ])
diff --git a/tests/test-fstatat.c b/tests/test-fstatat.c
index bd25905ca0..bbcbaae31e 100644
--- a/tests/test-fstatat.c
+++ b/tests/test-fstatat.c
@@ -101,6 +101,18 @@ main (_GL_UNUSED int argc, _GL_UNUSED char *argv[])
   ASSERT (0 <= dfd);
   ASSERT (test_stat_func (do_stat, false) == result);
   ASSERT (test_lstat_func (do_lstat, false) == result);
+#ifdef AT_EMPTY_PATH
+  struct stat dotst;
+  ASSERT (stat (".", &dotst) == 0);
+  for (int i = 0; i < 2; i++)
+    for (int j = 0; j < 2; j++)
+      {
+        struct stat st;
+        ASSERT (fstatat (i ? AT_FDCWD : dfd, j ? "" : NULL, &st, AT_EMPTY_PATH)
+                == 0);
+        ASSERT (st.st_dev == dotst.st_dev && st.st_ino == dotst.st_ino);
+      }
+#endif
   ASSERT (close (dfd) == 0);
 
   /* FIXME - add additional tests of dfd not at current directory.  */
-- 
2.54.0
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.