2025-10-27 Paul Eggert <eggert@cs.ucla.edu>
+ openat2: new module
+ This supports the openat2 system call of Linux 5.6 (2020) and
+ later. Although not yet exposed by glibc, the call is useful for
+ programs like GNU Tar that need to be paranoid about traversing
+ file names from untrusted sources. On platforms lacking
+ openat2, it is emulated in user space.
+ * lib/openat2.c, m4/openat2.m4, modules/openat2:
+ * modules/openat2-tests, tests/test-openat2.c: New files.
+ * lib/fcntl.in.h (struct open_how, RESOLVE_NO_XDEV)
+ (RESOLVE_NO_MAGICLINKS, RESOLVE_NO_SYMLINKS, RESOLVE_BENEATH)
+ (RESOLVE_IN_ROOT, RESOLVE_CACHED):
+ New type and constants, if <linux/openat2.h> does not define.
+ (openat2): New decls.
+ * m4/fcntl_h.m4 (gl_FCNTL_H, gl_FCNTL_H_REQUIRE_DEFAULTS)
+ (gl_FCNTL_H_DEFAULTS):
+ * modules/fcntl-h (fcntl.h):
+ Also check for openat2.
+
stdcountof-tests: pacify ODS 12.6
* tests/test-stdcountof-h.c (test_func) [__SUNPRO_C]: Omit tests
involving u"xxx" and u8"...", as Oracle Developer Studio 12.6
func_module getdtablesize
func_module isapipe
func_module openat-safer
+ func_module openat2
func_module pipe-posix
func_module pipe2
func_module pipe2-safer
# endif
#endif
+#if @GNULIB_OPENAT2@
+# if !defined RESOLVE_NO_XDEV && defined __has_include
+# if __has_include (<linux/openat2.h>)
+# include <linux/openat2.h>
+# endif
+# endif
+# ifndef RESOLVE_NO_XDEV
+struct open_how
+{
+# ifdef __UINT64_TYPE__
+ __UINT64_TYPE__ flags, mode, resolve;
+# else
+ unsigned long long int flags, mode, resolve;
+# endif
+};
+# define RESOLVE_NO_XDEV 0x01
+# define RESOLVE_NO_MAGICLINKS 0x02
+# define RESOLVE_NO_SYMLINKS 0x04
+# define RESOLVE_BENEATH 0x08
+# define RESOLVE_IN_ROOT 0x10
+# define RESOLVE_CACHED 0x20
+# endif
+
+# if !@HAVE_OPENAT2@
+_GL_FUNCDECL_SYS (openat2, int,
+ (int fd, char const *file, struct open_how const *how,
+ size_t size),
+ _GL_ARG_NONNULL ((2, 3)));
+# endif
+_GL_CXXALIAS_SYS (openat2, int,
+ (int fd, char const *file, struct open_how const *how,
+ size_t size));
+_GL_CXXALIASWARN (openat2);
+#elif defined GNULIB_POSIXCHECK
+# undef openat2
+# if HAVE_RAW_DECL_OPENAT2
+_GL_WARN_ON_USE (openat2, "openat2 is not portable - "
+ "use gnulib module openat2 for portability");
+# endif
+#endif
/* Fix up the FD_* macros, only known to be missing on mingw. */
--- /dev/null
+/* Open a file, with more flags than openat
+ Copyright 2025 Free Software Foundation, Inc.
+
+ This file 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.
+
+ This file 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 this program. If not, see <https://www.gnu.org/licenses/>. */
+
+/* written by Paul Eggert */
+
+#include <config.h>
+
+#include <fcntl.h>
+
+#include "eloop-threshold.h"
+#include "filename.h"
+#include "ialloc.h"
+#include "idx.h"
+#include "verify.h"
+
+#include <errno.h>
+#include <limits.h>
+#include <stdckdint.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#if defined __linux__ || defined __ANDROID__
+# include <sys/syscall.h>
+# include <linux/version.h>
+# if HAVE_SYS_VFS_H && HAVE_FSTATFS && HAVE_STRUCT_STATFS_F_TYPE
+# include <sys/vfs.h>
+/* Linux-specific constant from coreutils src/fs.h. */
+# define S_MAGIC_PROC 0x9FA0
+# endif
+#endif
+
+/* FSTAT_O_PATH_BUG is true if fstat fails on O_PATH file descriptors.
+ Although it can be dicey to use static checks for Linux kernel versions,
+ due to the dubious practice of building on newer kernels for older ones,
+ do it here anyway as the buggy kernels are rare (all EOLed by 2016)
+ and builders for them are unlikely to use the dubious practice.
+ Circa 2030 we should remove the old-kernel workarounds entirely. */
+#ifdef LINUX_VERSION_CODE
+# define FSTAT_O_PATH_BUG (KERNEL_VERSION (2, 6, 39) <= LINUX_VERSION_CODE \
+ && LINUX_VERSION_CODE < KERNEL_VERSION (3, 6, 0))
+#else
+# define FSTAT_O_PATH_BUG false
+#endif
+
+#ifndef E2BIG
+# define E2BIG EINVAL
+#endif
+
+#ifndef PATH_MAX
+# define PATH_MAX IDX_MAX
+#endif
+
+#ifndef O_ASYNC
+# define O_ASYNC 0
+#endif
+#ifndef O_CLOFORK
+# define O_CLOFORK 0
+#endif
+#ifndef O_LARGEFILE
+# define O_LARGEFILE 0
+#endif
+#ifndef O_NOCLOBBER
+# define O_NOCLOBBER 0
+#endif
+#ifndef O_PATH
+# define O_PATH 0
+#endif
+#ifndef O_RESOLVE_BENEATH /* A FreeBSD flag. */
+# define O_RESOLVE_BENEATH 0
+#endif
+#ifndef O_TMPFILE
+# define O_TMPFILE 0
+#endif
+#if O_PATH
+enum { O_PATHSEARCH = O_PATH };
+#else
+enum { O_PATHSEARCH = O_SEARCH };
+#endif
+
+/* Return true if the memory region at S of size N contains only zeros. */
+static bool
+memiszero (void const *s, idx_t n)
+{
+ /* Keep it simple, as N is typically zero. */
+ char const *p = s;
+ for (idx_t i = 0; i < n; i++)
+ if (p[i])
+ return false;
+ return true;
+}
+
+/* Return the negative of errno, helping the compiler about its sign. */
+static int
+negative_errno (void)
+{
+ int err = -errno;
+ assume (err < 0);
+ return err;
+}
+
+/* Make *BUF, which is of size BUFSIZE and which is heap-allocated
+ if not equal to STACKBUF, large enough to hold an object of NGROW + NTAIL.
+ Keep the last NTAIL bytes of *BUF; the rest of *BUF becomes uninitialized.
+ NTAIL must not exceed BUFSIZE.
+ Return the resulting buffer size, or a negative errno value
+ if the buffer could not be grown. */
+static ptrdiff_t
+maybe_grow (char **buf, idx_t bufsize, char *stackbuf,
+ idx_t ngrow, idx_t ntail)
+{
+ if (ngrow <= bufsize - ntail)
+ return bufsize;
+
+ idx_t needed;
+ if (ckd_add (&needed, ngrow, ntail))
+ return -ENOMEM;
+ idx_t s = ckd_add (&s, needed, needed >> 1) ? needed : s;
+ char *newbuf = imalloc (s);
+ if (!newbuf)
+ return negative_errno ();
+ char *oldbuf = *buf;
+ memcpy (newbuf + s - ntail, oldbuf + bufsize - ntail, ntail);
+ if (oldbuf != stackbuf)
+ free (oldbuf);
+ *buf = newbuf;
+ return s;
+}
+
+/* Store DIRFD's file status into *ST.
+ DIRFD is either AT_FDCWD or a nonnegative file descriptor.
+ Return 0 on success, -1 (setting errno) on failure. */
+static int
+dirstat (int dirfd, struct stat *st)
+{
+ /* Use fstatat only if fstat is buggy. fstatat is a bit slower,
+ and using it only on buggy hosts means openat2 need not depend on
+ Gnulib's fstatat module, as all systems with the fstat bug have
+ an fstatat that works well enough. */
+#if FSTAT_O_PATH_BUG
+ return fstatat (dirfd, ".", st);
+#else
+ return dirfd < 0 ? stat (".", st) : fstat (dirfd, st);
+#endif
+}
+
+/* Like openat2 (*FD, FILENAME, h, sizeof h) where h is
+ (struct open_how) { .flags = FLAGS, .resolve = RESOLVE, .mode = MODE },
+ except trust h's contents, advance *FD as we go,
+ use and update *BUF (originally pointing to a buffer of size BUFSIZE,
+ though it may be changed to point to a freshly allocated heap buffer),
+ and return the negative of the errno value on failure.
+ *FD and *BUF can be updated even on failure.
+ BUFSIZE must be at least 2. */
+static int
+do_openat2 (int *fd, char const *filename,
+ int flags, char resolve, mode_t mode,
+ char **buf, idx_t bufsize)
+{
+ int dfd = *fd;
+
+ /* RESOLVED_CACHED cannot be implemented properly in user space,
+ so pretend nothing is cached. */
+ if (resolve & RESOLVE_CACHED)
+ return -EAGAIN;
+
+ /* Put the file name being processed (including trailing NUL) at buffer end,
+ to simplify symlink resolution. */
+ idx_t filenamelen = strlen (filename);
+ if (!filenamelen)
+ return -ENOENT;
+ idx_t filenamesize = filenamelen + 1;
+ if (PATH_MAX < filenamesize)
+ return -ENAMETOOLONG;
+ char *stackbuf = *buf;
+ bufsize = maybe_grow (buf, bufsize, stackbuf, filenamesize, 0);
+ if (bufsize < 0)
+ return bufsize;
+
+ /* Pointer to buffer end. A common access is E[-I] where I is a
+ negative index relative to buffer end. */
+ char *e = *buf + bufsize;
+ memcpy (&e[-filenamesize], filename, filenamesize);
+
+ /* Directory depth below DFD. This is -1 if ".." ascended above
+ DFD at any point in the past, which can happen only if
+ neither RESOLVE_BENEATH nor RESOLVE_IN_ROOT is in effect. */
+ ptrdiff_t depth = 0;
+
+ /* DFD's device. UNKNOWN_DDEV if not acquired yet. If the actual
+ device number equals UNKNOWN_DDEV the code still works,
+ albeit more slowly. */
+ dev_t const UNKNOWN_DEV = -1;
+ dev_t ddev = UNKNOWN_DEV;
+
+ long int maxlinks = resolve & RESOLVE_NO_SYMLINKS ? 0 : __eloop_threshold ();
+
+ /* Iterates through file name components, possibly expanded by
+ symlink contents. */
+ while (true)
+ {
+ /* Make progress in interpreting &E[-FILENAMESIZE] as a file name.
+ If relative, it is relative to *FD.
+ FILENAMESIZE is positive.
+
+ Start by computing sizes of strings at the file name's end.
+ Use negations of sizes to index into E.
+ Here is an example file name and sizes of the trailing strings:
+
+ ///usr//bin/.////cat
+ F G H 1
+
+ As the '1' indicates, all sizes are positive
+ and include the trailing NUL at E[-1].
+
+ If there are file name components (the typical case),
+ -F <= -G < -H <= -1 and the first component
+ starts at E[-G] and ends just before E[-H].
+ Otherwise if the file name is nonempty,
+ -F < -G = -H = -1 and &E[-F] is a file system root.
+ Otherwise it is Solaris and we resolved an empty final symlink, and
+ -F = -G = -H = -1 and the empty file name is equivalent to ".".
+
+ F = G means the file name is relative to *FD;
+ otherwise the file name is not relative.
+
+ F (i.e., FILENAMESIZE) is the size of the file name. &E[-F] is what
+ is typically passed next to openat (with E[-H] set to NUL).
+
+ G is the size of the file name's suffix that starts with the name's
+ first component; &E[-G] addresses the first component.
+
+ H is the size of the suffix after the first component, i.e.,
+ E[-H] is the slash or NUL after the first component.
+
+ If there is no component, G and H are both 1. */
+ idx_t f = filenamesize;
+ idx_t g = f - FILE_SYSTEM_PREFIX_LEN (&e[-f]);
+ while (ISSLASH (e[-g]))
+ g--;
+ if (f != g)
+ {
+ /* The file name is not relative. */
+ if (resolve & RESOLVE_BENEATH)
+ return -EXDEV;
+ if (resolve & RESOLVE_IN_ROOT)
+ f = g;
+ if (*fd != dfd)
+ {
+ /* A non-relative symlink had been resolved at positive depth.
+ Forget its parent directory. */
+ close (*fd);
+ *fd = dfd;
+ }
+ }
+ idx_t h = g;
+ while (1 < h && !ISSLASH (e[- --h]))
+ continue;
+
+ /* Properties of the file name through the first component's end,
+ or to file name end if there is no component. */
+ bool leading_dot = e[-f] == '.';
+ bool dot_or_empty = f - h == leading_dot;
+ bool dotdot = leading_dot & (f - h == 2) & (e[-h - 1] == '.');
+ bool dotdot_as_dot = dotdot & !depth & !!(resolve & RESOLVE_IN_ROOT);
+ bool dotlike = dot_or_empty | dotdot_as_dot;
+
+ if (dotdot & !depth & !!(resolve & RESOLVE_BENEATH))
+ return -EXDEV;
+
+ /* NEXTF is the value of FILENAMESIZE next time through the loop,
+ unless a symlink intervenes. */
+ idx_t nextf = h;
+ while (ISSLASH (e[-nextf]))
+ nextf--;
+
+ /* FINAL means this is the last time through the loop,
+ unless a symlink intervenes. */
+ bool final = nextf == 1;
+
+ if (!final & dotlike)
+ {
+ /* A non-final component that acts like "."; skip it. */
+ filenamesize = nextf;
+ }
+ else if (!final & dotdot & (depth == 1))
+ {
+ /* A non-final ".." in a name like "x/../y/z" when "x" is an
+ existing non-symlink directory. As an optimization,
+ resolve it like "y/z". */
+ close (*fd);
+ *fd = dfd;
+ depth = 0;
+ filenamesize = nextf;
+ }
+ else
+ {
+ if (dotlike)
+ {
+ /* This is empty or the last component, and acts like ".".
+ Use "." regardless of whether it was "" or "." or "..". */
+ f = sizeof ".";
+ e[-f] = '.';
+ }
+
+ /* Open the current component, as either an internal directory or
+ the final open. Do not follow symlinks. */
+ int subflags = ((!final
+ ? O_PATHSEARCH | O_CLOEXEC | O_CLOFORK
+ : flags)
+ | O_NOFOLLOW | (e[-h] ? O_DIRECTORY : 0));
+ e[-h] = '\0';
+ int subfd = openat (*fd, &e[-f], subflags, mode);
+
+ if (subfd < 0)
+ {
+ if (maxlinks <= 0 || errno != ELOOP)
+ return negative_errno ();
+ maxlinks--;
+
+ /* A symlink and the symlink loop count is not exhausted.
+ Fail now if magic and if RESOLVE_NO_MAGIC_LINKS. */
+#ifdef S_MAGIC_PROC
+ if (resolve & RESOLVE_NO_MAGICLINKS)
+ {
+ bool relative = IS_RELATIVE_FILE_NAME (&e[-f]);
+ struct statfs st;
+ int r;
+ if (relative)
+ r = *fd < 0 ? statfs (".", &st) : fstatfs (*fd, &st);
+ else
+ {
+ char eg = e[-g];
+ e[-g] = '\0';
+ r = statfs (&e[-f], &st);
+ e[-g] = eg;
+ }
+ if (r < 0)
+ return negative_errno ();
+ if (st.f_type == S_MAGIC_PROC)
+ return -ELOOP;
+ }
+#endif
+
+ /* Read symlink contents into buffer start.
+ But if the root prefix might be needed,
+ leave room for it at buffer start. */
+ idx_t rootlen = f - g;
+ char *slink;
+ ssize_t slinklen;
+ for (idx_t more = rootlen + 1; ; more = bufsize - f + 1)
+ {
+ bufsize = maybe_grow (buf, bufsize, stackbuf, more, f);
+ if (bufsize < 0)
+ return bufsize;
+ e = *buf + bufsize;
+ slink = *buf + rootlen;
+ idx_t slinksize = bufsize - f - rootlen;
+ slinklen = readlinkat (*fd, &e[-f], slink, slinksize);
+ if (slinklen < 0)
+ return negative_errno ();
+ if (slinklen < slinksize)
+ break;
+ }
+
+ /* Compute KEPT, the number of trailing bytes in the file
+ name that will be appended to the symlink contents. */
+ idx_t kept;
+ if (slinklen == 0)
+ {
+ /* On Solaris empty symlinks act like ".".
+ On other platforms that allow them,
+ they fail with ENOENT. */
+#ifdef __sun
+ slink[slinklen] = '\0'; /* For IS_RELATIVE_FILE_NAME. */
+ kept = nextf;
+#else
+ return -ENOENT;
+#endif
+ }
+ else if (ISSLASH (slink[slinklen - 1]))
+ {
+ /* Skip any leading slashes in the kept bytes.
+ This can matter if the symlink contains only slashes,
+ because "//" and "/" can be distinct directories. */
+ kept = nextf;
+ }
+ else
+ kept = h;
+
+ if (ISSLASH ('\\'))
+ slink[slinklen] = '\0'; /* For IS_RELATIVE_FILE_NAME. */
+
+ /* Compute the new file name by concatenating:
+ - Any old root prefix if the symlink contents are relative.
+ - The symlink contents.
+ - The last KEPT bytes of the old file name.
+ The KEPT part is already in place. */
+ char const *prefix; /* [old root prefix +] symlink contents */
+ idx_t prefixlen;
+ if (IS_RELATIVE_FILE_NAME (slink))
+ {
+ prefix = memmove (*buf, &e[-f], rootlen);
+ prefixlen = rootlen + slinklen;
+ }
+ else
+ {
+ if (*fd != dfd)
+ {
+ close (*fd);
+ *fd = dfd;
+ }
+ prefix = slink;
+ prefixlen = slinklen;
+ }
+ filenamesize = prefixlen + kept;
+ if (PATH_MAX < filenamesize)
+ return -ENAMETOOLONG;
+ memmove (&e[-filenamesize], prefix, prefixlen);
+ }
+ else
+ {
+ if (*fd != dfd)
+ close (*fd);
+ *fd = subfd;
+
+ /* SUBFD is open to the file named by the current component.
+ If requested, require it to be in the same file system. */
+ if (resolve & RESOLVE_NO_XDEV)
+ {
+ struct stat st;
+ if (ddev == UNKNOWN_DEV)
+ {
+ if (dirstat (dfd, &st) < 0)
+ return negative_errno ();
+ ddev = st.st_dev;
+ }
+ if (dirstat (subfd, &st) < 0)
+ return negative_errno ();
+ if (st.st_dev != ddev)
+ return -EXDEV;
+ }
+
+ if (final)
+ {
+ *fd = dfd;
+ return subfd;
+ }
+
+ /* The component cannot be dotlike here, so if the depth is
+ nonnegative adjust it by +1 or -1. */
+ if (0 <= depth)
+ depth += dotdot ? -1 : 1;
+
+ filenamesize = nextf;
+ }
+ }
+ }
+}
+
+/* Like openat (DFD, FILENAME, HOW->flags, HOW->mode),
+ but with extra flags in *HOW, which is of size HOWSIZE. */
+int
+openat2 (int dfd, char const *filename,
+ struct open_how const *how, size_t howsize)
+{
+ int r;
+
+#ifdef SYS_openat2
+ r = syscall (SYS_openat2, dfd, filename, how, howsize);
+ if (! (r < 0 && errno == ENOSYS))
+ return r;
+
+ /* Keep going, to support the dubious practice of compiling for an
+ older kernel. The openat2 syscall was introduced in Linux 5.6.
+ Linux 5.4 LTS is EOLed at the end of 2025, so perhaps after that
+ we can simply return the syscall result instead of continuing. */
+#endif
+
+ /* Check for invalid arguments. Once the size test has succeeded,
+ *HOW's members are safe to access, so use & and | as there is
+ little point to using && or || when invalid arguments are rare.
+ (Other parts of this file also use & and | for similar reasons.)
+ These checks mimic those of the Linux kernel: when the Linux
+ kernel is overly generous, these checks are too. */
+ if (howsize < sizeof *how)
+ r = -EINVAL;
+ else if (!memiszero (how + 1, howsize - sizeof *how))
+ r = -E2BIG;
+ else if ((how->flags
+ & ~ (O_CLOFORK | O_CLOEXEC | O_DIRECTORY
+ | O_NOFOLLOW | O_PATH
+ | (how->flags & O_PATH
+ ? 0
+ : (O_ACCMODE | O_APPEND | O_ASYNC | O_BINARY
+ | O_CREAT | O_DIRECT | O_DSYNC | O_EXCL
+ | O_IGNORE_CTTY | O_LARGEFILE | O_NDELAY
+ | O_NOATIME | O_NOCLOBBER | O_NOCTTY
+ | O_NOLINK | O_NOLINKS | O_NONBLOCK | O_NOTRANS
+ | O_RSYNC | O_SYNC
+ | O_TEXT | O_TMPFILE | O_TRUNC | O_TTY_INIT))))
+ | ((how->flags & (O_DIRECTORY | O_CREAT))
+ == (O_DIRECTORY | O_CREAT))
+ | (!!(how->flags & O_TMPFILE & ~O_DIRECTORY)
+ & ((how->flags & (O_ACCMODE | O_DIRECTORY))
+ != (O_WRONLY | O_DIRECTORY))
+ & ((how->flags & (O_ACCMODE | O_DIRECTORY))
+ != (O_RDWR | O_DIRECTORY)))
+ | (how->mode
+ & ~ (how->flags & (O_CREAT | (O_TMPFILE & ~O_DIRECTORY))
+ ? (S_ISUID | S_ISGID | S_ISVTX
+ | S_IRWXU | S_IRWXG | S_IRWXO)
+ : 0))
+ | (how->resolve
+ & ~ (RESOLVE_BENEATH | RESOLVE_CACHED | RESOLVE_IN_ROOT
+ | RESOLVE_NO_MAGICLINKS | RESOLVE_NO_SYMLINKS
+ | RESOLVE_NO_XDEV))
+ | ((how->resolve & (RESOLVE_BENEATH | RESOLVE_IN_ROOT))
+ == (RESOLVE_BENEATH | RESOLVE_IN_ROOT)))
+ r = -EINVAL;
+ else
+ {
+ /* Args are valid so it is safe to use narrower types. */
+ int flags = how->flags;
+ char resolve = how->resolve;
+ mode_t mode = how->mode;
+
+ /* For speed use openat if it suffices, though it is unlikely a
+ caller would use openat2 when openat's simpler API would do. */
+ if (O_RESOLVE_BENEATH ? !(resolve & ~RESOLVE_BENEATH) : !resolve)
+ {
+ if (resolve & RESOLVE_BENEATH)
+ flags |= O_RESOLVE_BENEATH;
+ return openat (dfd, filename, flags, mode);
+ }
+
+ int fd = dfd;
+ char stackbuf[256];
+ char *buf = stackbuf;
+
+ r = do_openat2 (&fd, filename, flags, resolve, mode,
+ &buf, sizeof stackbuf);
+
+ if (fd != dfd)
+ close (fd);
+ if (buf != stackbuf)
+ free (buf);
+ }
+
+ if (r < 0)
+ {
+ errno = -r;
+ return -1;
+ }
+ return r;
+}
# fcntl_h.m4
-# serial 20
+# serial 21
dnl Copyright (C) 2006-2007, 2009-2025 Free Software Foundation, Inc.
dnl This file is free software; the Free Software Foundation
dnl gives unlimited permission to copy and/or distribute it,
dnl corresponding gnulib module is not in use, if it is not common
dnl enough to be declared everywhere.
gl_WARN_ON_USE_PREPARE([[#include <fcntl.h>
- ]], [fcntl openat])
+ ]], [fcntl openat openat2])
])
# gl_FCNTL_MODULE_INDICATOR([modulename])
gl_MODULE_INDICATOR_INIT_VARIABLE([GNULIB_NONBLOCKING])
gl_MODULE_INDICATOR_INIT_VARIABLE([GNULIB_OPEN])
gl_MODULE_INDICATOR_INIT_VARIABLE([GNULIB_OPENAT])
+ gl_MODULE_INDICATOR_INIT_VARIABLE([GNULIB_OPENAT2])
dnl Support Microsoft deprecated alias function names by default.
gl_MODULE_INDICATOR_INIT_VARIABLE([GNULIB_MDA_CREAT], [1])
gl_MODULE_INDICATOR_INIT_VARIABLE([GNULIB_MDA_OPEN], [1])
dnl Assume proper GNU behavior unless another module says otherwise.
HAVE_FCNTL=1; AC_SUBST([HAVE_FCNTL])
HAVE_OPENAT=1; AC_SUBST([HAVE_OPENAT])
+ HAVE_OPENAT2=0; AC_SUBST([HAVE_OPENAT2])
REPLACE_CREAT=0; AC_SUBST([REPLACE_CREAT])
REPLACE_FCNTL=0; AC_SUBST([REPLACE_FCNTL])
REPLACE_OPEN=0; AC_SUBST([REPLACE_OPEN])
--- /dev/null
+# openat2.m4
+# serial 1
+
+dnl Copyright 2025 Free Software Foundation, Inc.
+dnl This file is free software; the Free Software Foundation
+dnl gives unlimited permission to copy and/or distribute it,
+dnl with or without modifications, as long as this notice is preserved.
+dnl This file is offered as-is, without any warranty.
+
+# Written by Paul Eggert.
+
+AC_DEFUN([gl_FUNC_OPENAT2],
+[
+ AC_REQUIRE([gl_FCNTL_H_DEFAULTS])
+ AC_REQUIRE([gl_USE_SYSTEM_EXTENSIONS])
+ AC_REQUIRE([gl_FCNTL_O_FLAGS])
+ AC_CHECK_FUNCS_ONCE([openat2])
+ AS_CASE([$ac_cv_func_openat2],
+ [yes], [HAVE_OPENAT2=1])
+])
+
+# Prerequisites of lib/openat2.c.
+AC_DEFUN([gl_PREREQ_OPENAT2],
+[
+ AC_CHECK_FUNCS_ONCE([fstatfs])
+ AC_CHECK_HEADERS_ONCE([sys/vfs.h])
+ AS_CASE([$ac_cv_func_fstatfs,$ac_cv_header_sys_vfs_h],
+ [yes,yes],
+ [AC_CHECK_MEMBERS([struct statfs.f_type], [], [],
+ [[$ac_includes_default
+ #include <sys/vfs.h>
+ ]])])
+])
-e 's/@''GNULIB_NONBLOCKING''@/$(GNULIB_NONBLOCKING)/g' \
-e 's/@''GNULIB_OPEN''@/$(GNULIB_OPEN)/g' \
-e 's/@''GNULIB_OPENAT''@/$(GNULIB_OPENAT)/g' \
+ -e 's/@''GNULIB_OPENAT2''@/$(GNULIB_OPENAT2)/g' \
-e 's/@''GNULIB_MDA_CREAT''@/$(GNULIB_MDA_CREAT)/g' \
-e 's/@''GNULIB_MDA_OPEN''@/$(GNULIB_MDA_OPEN)/g' \
-e 's|@''HAVE_FCNTL''@|$(HAVE_FCNTL)|g' \
-e 's|@''HAVE_OPENAT''@|$(HAVE_OPENAT)|g' \
+ -e 's|@''HAVE_OPENAT2''@|$(HAVE_OPENAT2)|g' \
-e 's|@''REPLACE_CREAT''@|$(REPLACE_CREAT)|g' \
-e 's|@''REPLACE_FCNTL''@|$(REPLACE_FCNTL)|g' \
-e 's|@''REPLACE_OPEN''@|$(REPLACE_OPEN)|g' \
--- /dev/null
+Description:
+openat2() function: Open a file at a directory.
+
+Files:
+lib/openat2.c
+m4/openat2.m4
+
+Depends-on:
+fcntl-h
+extensions
+close [test $HAVE_OPENAT2 = 0]
+eloop-threshold [test $HAVE_OPENAT2 = 0]
+errno-h [test $HAVE_OPENAT2 = 0]
+filename [test $HAVE_OPENAT2 = 0]
+ialloc [test $HAVE_OPENAT2 = 0]
+idx [test $HAVE_OPENAT2 = 0]
+largefile [test $HAVE_OPENAT2 = 0]
+openat [test $HAVE_OPENAT2 = 0]
+readlinkat [test $HAVE_OPENAT2 = 0]
+stdckdint-h [test $HAVE_OPENAT2 = 0]
+sys_stat-h [test $HAVE_OPENAT2 = 0]
+unistd-h [test $HAVE_OPENAT2 = 0]
+verify [test $HAVE_OPENAT2 = 0]
+
+configure.ac:
+gl_FUNC_OPENAT2
+gl_CONDITIONAL([GL_COND_OBJ_OPENAT2],
+ [test $HAVE_OPENAT2 = 0])
+AM_COND_IF([GL_COND_OBJ_OPENAT], [
+ gl_PREREQ_OPENAT2
+])
+gl_FCNTL_MODULE_INDICATOR([openat2])
+
+Makefile.am:
+if GL_COND_OBJ_OPENAT2
+lib_SOURCES += openat2.c
+endif
+
+Include:
+<fcntl.h>
+
+Link:
+$(LTLIBINTL) when linking with libtool, $(LIBINTL) otherwise
+
+License:
+GPL
+
+Maintainer:
+all
--- /dev/null
+Files:
+tests/test-openat2.c
+tests/test-open.h
+tests/signature.h
+tests/macros.h
+
+Depends-on:
+close
+fcntl
+mkdirat
+stdcountof-h
+stdint-h
+symlinkat
+unlinkat
+
+configure.ac:
+AC_CHECK_DECLS_ONCE([alarm])
+
+Makefile.am:
+TESTS += test-openat2
+check_PROGRAMS += test-openat2
+test_openat2_LDADD = $(LDADD) @LIBINTL@
# define ALWAYS_INLINE
#endif
-/* This file is designed to test both open(n,buf[,mode]) and
- openat(AT_FDCWD,n,buf[,mode]). FUNC is the function to test.
+/* This file is designed to test open(n,buf[,mode]),
+ openat(dfd,n,buf[,mode]), and openat2(dfd,n,how,size).
+ FUNC is the function to test; for openat and openat2 it is a wrapper.
Assumes that BASE and ASSERT are already defined, and that
appropriate headers are already included. If PRINT, warn before
skipping symlink tests with status 77. */
--- /dev/null
+/* Test openat2.
+ Copyright 2025 Free Software Foundation, Inc.
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program 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 General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>. */
+
+/* Written by Paul Eggert and Adhemerval Zanella. */
+
+#include <config.h>
+
+#include <fcntl.h>
+
+#include "signature.h"
+SIGNATURE_CHECK (openat2, int, (int, char const *,
+ struct open_how const *, size_t));
+
+#include <errno.h>
+#include <stdarg.h>
+#include <stdcountof.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#if HAVE_DECL_ALARM
+# include <signal.h>
+#endif
+
+#include "macros.h"
+
+#define BASE "test-openat2.t"
+
+#include "test-open.h"
+
+static int dfd = AT_FDCWD;
+static uint_least64_t resolve;
+
+/* Wrapper around openat2 to test open behavior. */
+static int
+do_open (char const *name, int flags, ...)
+{
+ mode_t mode = 0;
+ if (flags & O_CREAT)
+ {
+ va_list arg;
+ va_start (arg, flags);
+
+ /* We have to use PROMOTED_MODE_T instead of mode_t, otherwise GCC 4
+ creates crashing code when 'mode_t' is smaller than 'int'. */
+ mode = va_arg (arg, PROMOTED_MODE_T);
+
+ va_end (arg);
+ }
+
+ struct open_how how = { .flags = flags, .mode = mode, .resolve = resolve };
+ return openat2 (dfd, name, &how, sizeof how);
+}
+
+#define temp_dir BASE "temp_dir"
+
+static void
+do_prepare ()
+{
+ /*
+ Construct a test directory with the following structure:
+
+ temp_dir/
+ |- escaping_link -> /tmp
+ |- escaping_link_2 -> escaping_link
+ |- some_file
+ |- invalid_link -> some_file/invalid
+ |- valid_link -> some_file
+ |- subdir/
+ |- some_file
+ */
+
+ ASSERT (mkdirat (AT_FDCWD, temp_dir, 0700) == 0);
+ dfd = openat2 (AT_FDCWD, temp_dir,
+ (&(struct open_how) { .flags = O_RDONLY | O_DIRECTORY }),
+ sizeof (struct open_how));
+ ASSERT (0 <= dfd);
+ ASSERT (symlinkat ("/", dfd, "escaping_link") == 0);
+ ASSERT (symlinkat ("escaping_link", dfd, "escaping_link_2") == 0);
+ ASSERT (symlinkat ("some_file/invalid", dfd, "invalid_link") == 0);
+ ASSERT (symlinkat ("some_file", dfd, "valid_link") == 0);
+ ASSERT (mkdirat (dfd, "subdir", 0700) == 0);
+ ASSERT (close (openat2 (dfd, "some_file",
+ (&(struct open_how) { .flags = O_CREAT,
+ .mode = 0600 }),
+ sizeof (struct open_how)))
+ == 0);
+ ASSERT (close (openat2 (dfd, "subdir/some_file",
+ (&(struct open_how) { .flags = O_CREAT,
+ .mode = 0600 }),
+ sizeof (struct open_how)))
+ == 0);
+}
+
+static void
+do_test_struct ()
+{
+ static struct struct_test
+ {
+ struct open_how_ext
+ {
+ struct open_how inner;
+ int extra1;
+ int extra2;
+ int extra3;
+ } arg;
+ size_t size;
+ int err;
+ } const tests[] =
+ {
+ {
+ /* Zero size. */
+ .arg.inner.flags = O_RDONLY,
+ .size = 0,
+ .err = EINVAL,
+ },
+ {
+ /* Normal struct. */
+ .arg.inner.flags = O_RDONLY,
+ .size = sizeof (struct open_how),
+ },
+ {
+ /* Larger struct, zeroed out the unused values. */
+ .arg.inner.flags = O_RDONLY,
+ .size = sizeof (struct open_how_ext),
+ },
+ {
+ /* Larger struct, non-zeroed out the unused values. */
+ .arg.inner.flags = O_RDONLY,
+ .arg.extra1 = 0xdeadbeef,
+ .size = sizeof (struct open_how_ext),
+ .err = E2BIG,
+ },
+ {
+ /* Larger struct, non-zeroed out the unused values. */
+ .arg.inner.flags = O_RDONLY,
+ .arg.extra2 = 0xdeadbeef,
+ .size = sizeof (struct open_how_ext),
+ .err = E2BIG,
+ },
+ };
+
+ for (struct struct_test const *t = tests; t < tests + countof (tests); t++)
+ {
+ int fd = openat2 (AT_FDCWD, ".", (struct open_how *) &t->arg, t->size);
+ if (!t->err)
+ ASSERT (close (fd) == 0);
+ else
+ {
+ ASSERT (errno == t->err);
+ ASSERT (fd == -1);
+ }
+ }
+}
+
+static void
+do_test_flags (void)
+{
+ static struct flag_test
+ {
+ char const *filename;
+ struct open_how how;
+ int err;
+ } const tests[] =
+ {
+#ifdef O_PATH
+# ifdef O_TMPFILE
+ /* O_TMPFILE is incompatible with O_PATH and O_CREAT. */
+ {
+ .how.flags = O_TMPFILE | O_PATH | O_RDWR,
+ .err = EINVAL },
+ {
+ .how.flags = O_TMPFILE | O_CREAT | O_RDWR,
+ .err = EINVAL },
+# endif
+
+ /* O_PATH only permits certain other flags to be set ... */
+ {
+ .how.flags = O_PATH | O_CLOEXEC
+ },
+ {
+ .how.flags = O_PATH | O_DIRECTORY
+ },
+ {
+ .how.flags = O_PATH | O_NOFOLLOW
+ },
+ /* ... and others are absolutely not permitted. */
+ {
+ .how.flags = O_PATH | O_RDWR,
+ .err = EINVAL },
+ {
+ .how.flags = O_PATH | O_CREAT,
+ .err = EINVAL },
+ {
+ .how.flags = O_PATH | O_EXCL,
+ .err = EINVAL },
+ {
+ .how.flags = O_PATH | O_NOCTTY,
+ .err = EINVAL },
+ {
+ .how.flags = O_PATH | O_DIRECT,
+ .err = EINVAL },
+#endif
+
+ /* ->mode must only be set with O_{CREAT,TMPFILE}. */
+ {
+ .how.flags = O_RDONLY,
+ .how.mode = 0600,
+ .err = EINVAL },
+#ifdef O_PATH
+ {
+ .how.flags = O_PATH,
+ .how.mode = 0600,
+ .err = EINVAL },
+#endif
+ {
+ .how.flags = O_CREAT,
+ .how.mode = 0600 },
+#ifdef O_TMPFILE
+ {
+ .how.flags = O_TMPFILE | O_RDWR,
+ .how.mode = 0600 },
+#endif
+ /* ->mode must only contain 07777 bits. */
+ {
+ .how.flags = O_CREAT, .how.mode = 0xFFFF, .err = EINVAL },
+ {
+ .how.flags = O_CREAT, .how.mode = 0xc000000000000000,
+ .err = EINVAL },
+#ifdef O_TMPFILE
+ {
+ .how.flags = O_TMPFILE | O_RDWR, .how.mode = 0x10000,
+ .err = EINVAL },
+ {
+ .how.flags = O_TMPFILE | O_RDWR,
+ .how.mode = 0xa00000000000,
+ .err = EINVAL
+ },
+#endif
+
+ /* ->resolve flags must not conflict. */
+ {
+ .how.flags = O_RDONLY,
+ .how.resolve = RESOLVE_BENEATH | RESOLVE_IN_ROOT,
+ .err = EINVAL
+ },
+
+ /* ->resolve must only contain RESOLVE_* flags. */
+ {
+ .how.flags = O_RDONLY,
+ .how.resolve = 0x1337,
+ .err = EINVAL
+ },
+ {
+ .how.flags = O_CREAT,
+ .how.resolve = 0x1337,
+ .err = EINVAL
+ },
+#ifdef O_TMPFILE
+ {
+ .how.flags = O_TMPFILE | O_RDWR,
+ .how.resolve = 0x1337,
+ .err = EINVAL
+ },
+#endif
+#ifdef O_PATH
+ {
+ .how.flags = O_PATH,
+ .how.resolve = 0x1337,
+ .err = EINVAL
+ },
+#endif
+
+ /* currently unknown upper 32 bit rejected. */
+ {
+ .how.flags = O_RDONLY | (1ull << 63),
+ .how.resolve = 0,
+ .err = EINVAL
+ },
+ };
+
+ for (struct flag_test const *t = tests; t < tests + countof (tests); t++)
+ {
+ char const *filename = t->how.flags & O_CREAT ? "created" : ".";
+ if (t->how.flags & O_CREAT)
+ unlinkat (dfd, filename, 0);
+
+ int fd = openat2 (dfd, filename, &t->how, sizeof (struct open_how));
+ if (fd < 0 && errno == EOPNOTSUPP)
+ {
+ /* Skip the testcase if FS does not support the operation (e.g.
+ valid O_TMPFILE on NFS). */
+ continue;
+ }
+
+ if (!t->err)
+ ASSERT (close (fd) == 0);
+ else
+ {
+ ASSERT (errno == t->err);
+ ASSERT (fd == -1);
+ }
+ }
+}
+
+static void
+do_test_resolve (void)
+{
+ int fd;
+
+ /* ESCAPING_LINK links to /tmp, which escapes the temporary test
+ directory. */
+ fd = openat2 (dfd,
+ "escaping_link",
+ (&(struct open_how)
+ {
+ .resolve = RESOLVE_BENEATH | RESOLVE_NO_SYMLINKS,
+ }),
+ sizeof (struct open_how));
+ ASSERT (errno == ELOOP || errno == EXDEV);
+ ASSERT (fd == -1);
+
+ /* Same as before, ESCAPING_LINK_2 links to ESCAPING_LINK. */
+ fd = openat2 (dfd,
+ "escaping_link_2",
+ (&(struct open_how)
+ {
+ .resolve = RESOLVE_BENEATH | RESOLVE_NO_SYMLINKS,
+ }),
+ sizeof (struct open_how));
+ ASSERT (errno == ELOOP || errno == EXDEV);
+ ASSERT (fd == -1);
+
+ /* ESCAPING_LINK links to the temporary directory itself (dfd). */
+ fd = openat2 (dfd,
+ "escaping_link",
+ (&(struct open_how)
+ {
+ .resolve = RESOLVE_BENEATH | RESOLVE_NO_SYMLINKS,
+ }),
+ sizeof (struct open_how));
+ ASSERT (errno == ELOOP || errno == EXDEV);
+ ASSERT (fd == -1);
+
+ /* Although it points to a valid file in same path, the link refers to
+ an absolute path. */
+ fd = openat2 (dfd,
+ "invalid_link",
+ (&(struct open_how)
+ {
+ .resolve = RESOLVE_BENEATH | RESOLVE_NO_SYMLINKS,
+ }),
+ sizeof (struct open_how));
+ ASSERT (errno == ELOOP || errno == EXDEV);
+ ASSERT (fd == -1);
+
+ fd = openat2 (dfd,
+ "valid_link",
+ (&(struct open_how)
+ {
+ .resolve = RESOLVE_BENEATH | RESOLVE_NO_SYMLINKS,
+ }),
+ sizeof (struct open_how));
+ ASSERT (errno == ELOOP);
+ ASSERT (fd == -1);
+
+ fd = openat2 (dfd,
+ "should-not-work",
+ (&(struct open_how)
+ {
+ .resolve = RESOLVE_IN_ROOT | RESOLVE_NO_SYMLINKS,
+ }),
+ sizeof (struct open_how));
+ ASSERT (errno == ELOOP | errno == ENOENT);
+ ASSERT (fd == -1);
+
+ {
+ int subdfd = openat2 (dfd,
+ "subdir",
+ (&(struct open_how)
+ {
+ .flags = O_RDONLY | O_DIRECTORY,
+ .resolve = RESOLVE_IN_ROOT | RESOLVE_NO_SYMLINKS,
+ }),
+ sizeof (struct open_how));
+ ASSERT (0 <= subdfd);
+
+ /* Open the file within the subdir with both tst-openat2
+ and tst-openat2/subdir file descriptors. */
+ fd = openat2 (subdfd,
+ "some_file",
+ (&(struct open_how)
+ {
+ .resolve = RESOLVE_IN_ROOT,
+ }),
+ sizeof (struct open_how));
+ ASSERT (close (fd) == 0);
+
+ fd = openat2 (dfd,
+ "subdir/some_file",
+ (&(struct open_how)
+ {
+ .resolve = RESOLVE_IN_ROOT,
+ }),
+ sizeof (struct open_how));
+ ASSERT (close (fd) == 0);
+ }
+}
+
+static void
+do_test_basic ()
+{
+ int fd;
+
+ fd = openat2 (dfd,
+ "some-file",
+ (&(struct open_how)
+ {
+ .flags = O_CREAT|O_RDWR|O_EXCL,
+ .mode = 0666,
+ }),
+ sizeof (struct open_how));
+ ASSERT (0 <= fd);
+ ASSERT (write (fd, "hello", 5) == 5);
+
+ /* Before closing the file, try using this file descriptor to open
+ another file. This must fail. */
+ {
+ int fd2 = openat2 (fd,
+ "should-not-work",
+ (&(struct open_how)
+ {
+ .flags = O_CREAT|O_RDWR|O_EXCL,
+ .mode = 0666,
+ }),
+ sizeof (struct open_how));
+ ASSERT (errno == ENOTDIR);
+ ASSERT (fd2 == -1);
+ }
+
+ ASSERT (unlinkat (dfd, "some-file", 0) == 0);
+
+ ASSERT (unlinkat (dfd, "escaping_link", 0) == 0);
+ ASSERT (unlinkat (dfd, "escaping_link_2", 0) == 0);
+ ASSERT (unlinkat (dfd, "invalid_link", 0) == 0);
+ ASSERT (unlinkat (dfd, "some_file", 0) == 0);
+ ASSERT (unlinkat (dfd, "subdir/some_file", 0) == 0);
+ ASSERT (unlinkat (dfd, "subdir", AT_REMOVEDIR) == 0);
+ ASSERT (unlinkat (dfd, "valid_link", 0) == 0);
+
+ ASSERT (close (dfd) == 0);
+
+ fd = openat2 (dfd,
+ "some-file",
+ (&(struct open_how)
+ {
+ .flags = O_CREAT|O_RDWR|O_EXCL,
+ .mode = 0666,
+ }),
+ sizeof (struct open_how));
+ ASSERT (errno == EBADF);
+ ASSERT (fd == -1);
+
+ ASSERT (unlinkat (AT_FDCWD, temp_dir, AT_REMOVEDIR) == 0);
+}
+
+int
+main ()
+{
+ int result;
+ struct open_how ro = { .flags = O_RDONLY };
+
+ /* Test behavior for invalid file descriptors. */
+ {
+ errno = 0;
+ ASSERT (openat2 (AT_FDCWD == -1 ? -2 : -1, "foo", &ro, sizeof ro) == -1);
+ ASSERT (errno == EBADF);
+ }
+ {
+ close (99);
+ errno = 0;
+ ASSERT (openat2 (99, "foo", &ro, sizeof ro) == -1);
+ ASSERT (errno == EBADF);
+ }
+
+ /* Basic checks. */
+ result = test_open (do_open, false);
+ dfd = open (".", O_RDONLY);
+ ASSERT (0 <= dfd);
+ ASSERT (test_open (do_open, false) == result);
+ ASSERT (close (dfd) == 0);
+
+ do_prepare ();
+ do_test_struct ();
+ do_test_flags ();
+ do_test_resolve ();
+ do_test_basic ();
+
+ /* Check that even when *-safer modules are in use, plain openat2 can
+ land in fd 0. Do this test last, since it is destructive to
+ stdin. */
+ ASSERT (close (STDIN_FILENO) == 0);
+ ASSERT (openat2 (AT_FDCWD, ".", &ro, sizeof ro) == STDIN_FILENO);
+ {
+ dfd = open (".", O_RDONLY);
+ ASSERT (STDIN_FILENO < dfd);
+ ASSERT (chdir ("..") == 0);
+ ASSERT (close (STDIN_FILENO) == 0);
+ ASSERT (openat2 (dfd, ".", &ro, sizeof ro) == STDIN_FILENO);
+ ASSERT (close (dfd) == 0);
+ }
+ return result ? result : test_exit_status;
+}