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
