[PATCH v2] linux: Fix stat layout divergence under pre-POSIX.1-2008 macros [BZ #32119]
Adhemerval Zanella <[email protected]>
| Newsgroups | gmane.comp.lib.glibc.alpha |
|---|---|
| Message-ID | <[email protected]> |
The struct stat layout must not depend on the feature-test macros in
use. Currently there are three issues:
- bits/struct_stat_time64_helper.h tests __BYTE_ORDER without
including <bits/endian.h>. In strict pre-POSIX.1-2008 modes both
operands are undefined, the big-endian member order is selected
on little-endian targets, which displaces the time64 nanosecond
fields on targets whose bits/struct_stat.h does not already include
<bits/endian.h> (x86, mips o32/n32, microblazeel).
- The same helper adds two reserved words only when POSIX.1-2008
interfaces are hidden, making the public time64 structure larger
under older feature-test macros.
- The generic bits/struct_stat.h declares the strict-mode timestamps
as a bare seconds/nanoseconds pair with no endianness handling or
explicit padding. On 32-bit ports with 64-bit time_t whose ABI
aligns 64-bit types to 4 bytes, the timestamps end up packed 12
bytes apart instead of 16 (arc); on big-endian the nanoseconds also
land on the wrong side of the padding (or1k). This triggers with
plain -D_POSIX_C_SOURCE=200112L, without any _TIME_BITS opt-in,
since time_t is always 64-bit on such ports.
Fix the helper by including <bits/endian.h>, and give the generic
header the same endianness-aware nanosecond padding as
<bits/types/struct_timespec.h>.
For the reserved words, instead of removing them unify all
feature-test-macro modes on the largest layout. The internal
__stat64_t64 also carries them, so libc-owned buffers written by
application callbacks compiled against the public headers keep
working (for instance glob with GLOB_ALTDIRFUNC).
The stat functions clear and fill the structure only up to the
reserved words (STAT64_T64_FILL_SIZE), so previously built objects
using the smaller default-mode structure remain correct
(newly built objects will over-allocate trailing bytes that are
never written).
The resulting sizeof (struct stat) changes, with _TIME_BITS=64 for
the first two rows (the time32 layouts are unchanged) and with any
feature-test macro combination on the time64-only arc and or1k:
ABI default and pre-POSIX.1-2008
POSIX.1-2008 strict modes
i686, csky, m68k, microblaze,
sh 108 -> 116 116 (unchanged)
arm, hppa, mips o32/n32,
powerpc32, sparc32 112 -> 120 120 (unchanged)
arc, or1k 128 (unchanged) 116 -> 128
Co-authored-by: Matthias Goergens <[email protected]>
---
sysdeps/unix/sysv/linux/Makefile | 11 +++
sysdeps/unix/sysv/linux/bits/struct_stat.h | 36 +++++---
.../linux/bits/struct_stat_time64_helper.h | 11 ++-
sysdeps/unix/sysv/linux/fstatat64.c | 8 +-
sysdeps/unix/sysv/linux/internal-stat.h | 14 +++
.../unix/sysv/linux/tst-stat-layout-time64.py | 91 +++++++++++++++++++
6 files changed, 153 insertions(+), 18 deletions(-)
create mode 100644 sysdeps/unix/sysv/linux/tst-stat-layout-time64.py
diff --git a/sysdeps/unix/sysv/linux/Makefile b/sysdeps/unix/sysv/linux/Makefile
index 14a56d5cc3f..a5811977d4a 100644
--- a/sysdeps/unix/sysv/linux/Makefile
+++ b/sysdeps/unix/sysv/linux/Makefile
@@ -413,6 +413,17 @@ $(objpfx)tst-sched-consts.out: ../sysdeps/unix/sysv/linux/tst-sched-consts.py
< /dev/null > $@ 2>&1; $(evaluate-test)
$(objpfx)tst-sched-consts.out: $(sysdeps-linux-python-deps)
+tests-special += \
+ $(objpfx)tst-stat-layout-time64.out \
+ # tests-special
+$(objpfx)tst-stat-layout-time64.out: \
+ ../sysdeps/unix/sysv/linux/tst-stat-layout-time64.py
+ $(sysdeps-linux-python) \
+ ../sysdeps/unix/sysv/linux/tst-stat-layout-time64.py \
+ $(sysdeps-linux-python-cc) \
+ < /dev/null > $@ 2>&1; $(evaluate-test)
+$(objpfx)tst-stat-layout-time64.out: $(sysdeps-linux-python-deps)
+
tst-rseq-disable-TUNABLES += glibc.pthread.rseq=0
tst-rseq-disable-static-TUNABLES += glibc.pthread.rseq=0
diff --git a/sysdeps/unix/sysv/linux/bits/struct_stat.h b/sysdeps/unix/sysv/linux/bits/struct_stat.h
index e912c3f6ba5..cdbe1f8f762 100644
--- a/sysdeps/unix/sysv/linux/bits/struct_stat.h
+++ b/sysdeps/unix/sysv/linux/bits/struct_stat.h
@@ -41,6 +41,22 @@
int __##name##_pad __attribute__((__aligned__ (__alignof__ (type64)))); type name
#endif
+/* The pre-POSIX.1-2008 timestamp fields must match the layout of the
+ 'struct timespec' members used in the POSIX.1-2008 case, including the
+ padding required when the seconds field is wider than the word size. */
+#if __WORDSIZE == 64 \
+ || (defined __SYSCALL_WORDSIZE && __SYSCALL_WORDSIZE == 64) \
+ || (__TIMESIZE == 32 && !defined __USE_TIME64_REDIRECTS)
+# define __fieldts(name) \
+ __time_t name; unsigned long int name ## nsec
+#elif __BYTE_ORDER == __BIG_ENDIAN
+# define __fieldts(name) \
+ __time_t name; int: 32; unsigned long int name ## nsec
+#else
+# define __fieldts(name) \
+ __time_t name; unsigned long int name ## nsec; int: 32
+#endif
+
struct stat
{
__dev_t st_dev; /* Device. */
@@ -69,12 +85,9 @@ struct stat
# define st_mtime st_mtim.tv_sec
# define st_ctime st_ctim.tv_sec
#else
- __time_t st_atime; /* Time of last access. */
- unsigned long int st_atimensec; /* Nscecs of last access. */
- __time_t st_mtime; /* Time of last modification. */
- unsigned long int st_mtimensec; /* Nsecs of last modification. */
- __time_t st_ctime; /* Time of last status change. */
- unsigned long int st_ctimensec; /* Nsecs of last status change. */
+ __fieldts (st_atime); /* Time of last access. */
+ __fieldts (st_mtime); /* Time of last modification. */
+ __fieldts (st_ctime); /* Time of last status change. */
#endif
int __glibc_reserved[2];
};
@@ -107,17 +120,16 @@ struct stat64
struct timespec st_mtim; /* Time of last modification. */
struct timespec st_ctim; /* Time of last status change. */
#else
- __time_t st_atime; /* Time of last access. */
- unsigned long int st_atimensec; /* Nscecs of last access. */
- __time_t st_mtime; /* Time of last modification. */
- unsigned long int st_mtimensec; /* Nsecs of last modification. */
- __time_t st_ctime; /* Time of last status change. */
- unsigned long int st_ctimensec; /* Nsecs of last status change. */
+ __fieldts (st_atime); /* Time of last access. */
+ __fieldts (st_mtime); /* Time of last modification. */
+ __fieldts (st_ctime); /* Time of last status change. */
#endif
int __glibc_reserved[2];
};
#endif
+#undef __fieldts
+
/* Tell code we have these members. */
#define _STATBUF_ST_BLKSIZE
#define _STATBUF_ST_RDEV
diff --git a/sysdeps/unix/sysv/linux/bits/struct_stat_time64_helper.h b/sysdeps/unix/sysv/linux/bits/struct_stat_time64_helper.h
index e0b78b314ad..2f0bab27e72 100644
--- a/sysdeps/unix/sysv/linux/bits/struct_stat_time64_helper.h
+++ b/sysdeps/unix/sysv/linux/bits/struct_stat_time64_helper.h
@@ -16,6 +16,8 @@
License along with the GNU C Library. If not, see
<https://www.gnu.org/licenses/>. */
+#include <bits/endian.h>
+
/* Content of internal __stat64_t64 struct. */
__dev_t st_dev; /* Device. */
__ino64_t st_ino; /* file serial number. */
@@ -59,8 +61,11 @@
__fieldts64 (st_mtime);
__fieldts64 (st_ctime);
- unsigned long int __glibc_reserved4;
- unsigned long int __glibc_reserved5;
-
# undef __fieldts64
#endif
+
+ /* Trailing reserved words, never set by the stat functions. They keep
+ sizeof (struct stat) as the largest one for all feature-test-macro
+ modes. */
+ unsigned long int __glibc_reserved4;
+ unsigned long int __glibc_reserved5;
diff --git a/sysdeps/unix/sysv/linux/fstatat64.c b/sysdeps/unix/sysv/linux/fstatat64.c
index efa5eb871b6..9427c779185 100644
--- a/sysdeps/unix/sysv/linux/fstatat64.c
+++ b/sysdeps/unix/sysv/linux/fstatat64.c
@@ -52,7 +52,7 @@ fstatat64_time64_statx (int fd, const char *file, struct __stat64_t64 *buf,
if (r != 0)
return r;
- *buf = (struct __stat64_t64) {
+ struct __stat64_t64 st = {
.st_dev = __gnu_dev_makedev (tmp.stx_dev_major, tmp.stx_dev_minor),
.st_rdev = __gnu_dev_makedev (tmp.stx_rdev_major, tmp.stx_rdev_minor),
.st_ino = tmp.stx_ino,
@@ -70,6 +70,7 @@ fstatat64_time64_statx (int fd, const char *file, struct __stat64_t64 *buf,
.st_blocks = tmp.stx_blocks,
.st_blksize = tmp.stx_blksize,
};
+ memcpy (buf, &st, STAT64_T64_FILL_SIZE);
return r;
}
@@ -110,8 +111,9 @@ fstatat64_time64_stat (int fd, const char *file, struct __stat64_t64 *buf,
r = INTERNAL_SYSCALL_CALL (fstatat64, fd, file, &st64, flag);
if (r == 0)
{
- /* Clear both pad and reserved fields. */
- memset (buf, 0, sizeof (*buf));
+ /* Clear both pad and reserved fields, but not the trailing
+ reserved words that are not part of the fill area. */
+ memset (buf, 0, STAT64_T64_FILL_SIZE);
buf->st_dev = st64.st_dev,
buf->st_ino = st64.st_ino;
diff --git a/sysdeps/unix/sysv/linux/internal-stat.h b/sysdeps/unix/sysv/linux/internal-stat.h
index 908da1b33c5..7a271046c94 100644
--- a/sysdeps/unix/sysv/linux/internal-stat.h
+++ b/sysdeps/unix/sysv/linux/internal-stat.h
@@ -16,6 +16,7 @@
License along with the GNU C Library; if not, see
<https://www.gnu.org/licenses/>. */
+#include <stddef.h>
#include <sysdep.h>
#include <stat_t64_cp.h>
#include <kernel_stat.h>
@@ -29,3 +30,16 @@
#else
# define FSTATAT_USE_STATX 0
#endif
+
+/* With __TIMESIZE != 64 the time64 struct stat ends with two trailing
+ reserved words (that the stat functions never set). Objects built against
+ previous versions of the default feature-test-macro layout allocate the
+ structure without them.
+ With __TIMESIZE == 64 the structure is the kernel one and must be fully
+ set. */
+#if __TIMESIZE != 64
+# define STAT64_T64_FILL_SIZE \
+ offsetof (struct __stat64_t64, __glibc_reserved4)
+#else
+# define STAT64_T64_FILL_SIZE sizeof (struct __stat64_t64)
+#endif
diff --git a/sysdeps/unix/sysv/linux/tst-stat-layout-time64.py b/sysdeps/unix/sysv/linux/tst-stat-layout-time64.py
new file mode 100644
index 00000000000..288a03bb661
--- /dev/null
+++ b/sysdeps/unix/sysv/linux/tst-stat-layout-time64.py
@@ -0,0 +1,91 @@
+#!/usr/bin/python3
+# Check that feature-test macros do not change the time64 stat layout.
+# Copyright (C) 2026 Free Software Foundation, Inc.
+# This file is part of the GNU C Library.
+#
+# The GNU C Library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# The GNU C Library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with the GNU C Library; if not, see
+# <https://www.gnu.org/licenses/>.
+
+import argparse
+import sys
+
+import glibcextract
+
+MODES = {
+ 'POSIX.1-1996': '#define _POSIX_C_SOURCE 199506L',
+ 'POSIX.1-2001': '#define _POSIX_C_SOURCE 200112L',
+ 'POSIX.1-2008': '#define _POSIX_C_SOURCE 200809L',
+ 'XPG6': '#define _XOPEN_SOURCE 600',
+}
+
+
+def compute_stat_layout(cc, mode_define):
+ sym_data = [
+ '#undef _GNU_SOURCE',
+ mode_define,
+ '#define _TIME_BITS 64',
+ '#define _FILE_OFFSET_BITS 64',
+ '#include <stddef.h>',
+ '#include <sys/stat.h>',
+ 'START',
+ ('sizeof_stat', 'sizeof (struct stat)'),
+ ('st_dev', 'offsetof (struct stat, st_dev)'),
+ ('st_ino', 'offsetof (struct stat, st_ino)'),
+ ('st_mode', 'offsetof (struct stat, st_mode)'),
+ ('st_nlink', 'offsetof (struct stat, st_nlink)'),
+ ('st_uid', 'offsetof (struct stat, st_uid)'),
+ ('st_gid', 'offsetof (struct stat, st_gid)'),
+ ('st_rdev', 'offsetof (struct stat, st_rdev)'),
+ ('st_size', 'offsetof (struct stat, st_size)'),
+ ('st_blksize', 'offsetof (struct stat, st_blksize)'),
+ ('st_blocks', 'offsetof (struct stat, st_blocks)'),
+ ('st_atime', 'offsetof (struct stat, st_atime)'),
+ ('st_mtime', 'offsetof (struct stat, st_mtime)'),
+ ('st_ctime', 'offsetof (struct stat, st_ctime)'),
+ '#ifdef __USE_XOPEN2K8',
+ ('st_atimensec', 'offsetof (struct stat, st_atim.tv_nsec)'),
+ ('st_mtimensec', 'offsetof (struct stat, st_mtim.tv_nsec)'),
+ ('st_ctimensec', 'offsetof (struct stat, st_ctim.tv_nsec)'),
+ '#else',
+ ('st_atimensec', 'offsetof (struct stat, st_atimensec)'),
+ ('st_mtimensec', 'offsetof (struct stat, st_mtimensec)'),
+ ('st_ctimensec', 'offsetof (struct stat, st_ctimensec)'),
+ '#endif',
+ ]
+ return glibcextract.compute_c_consts(sym_data, cc)
+
+
+def main():
+ parser = argparse.ArgumentParser(
+ description='Check that feature-test macros do not change '
+ 'the time64 stat layout.')
+ parser.add_argument('--cc', metavar='CC',
+ help='C compiler (including options) to use')
+ args = parser.parse_args()
+ default_layout = compute_stat_layout(args.cc, '#define _GNU_SOURCE 1')
+ status = 0
+ for mode, mode_define in sorted(MODES.items()):
+ mode_layout = compute_stat_layout(args.cc, mode_define)
+ for name, value in default_layout.items():
+ if mode_layout[name] != value:
+ print('FAIL: %s: %s is %s, %s in default mode'
+ % (mode, name, mode_layout[name], value))
+ status = 1
+ if status == 0:
+ print('PASS: struct stat layout is feature-test-macro invariant')
+ sys.exit(status)
+
+
+if __name__ == '__main__':
+ main()
--
2.53.0