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.
---
 ChangeLog             |  18 ++
 MODULES.html.sh       |   1 +
 lib/fcntl.in.h        |  40 +++
 lib/openat2.c         | 569 ++++++++++++++++++++++++++++++++++++++++++
 m4/fcntl_h.m4         |   6 +-
 m4/openat2.m4         |  33 +++
 modules/fcntl-h       |   2 +
 modules/openat2       |  49 ++++
 modules/openat2-tests |  22 ++
 tests/test-open.h     |   5 +-
 tests/test-openat2.c  | 527 ++++++++++++++++++++++++++++++++++++++
 11 files changed, 1268 insertions(+), 4 deletions(-)
 create mode 100644 lib/openat2.c
 create mode 100644 m4/openat2.m4
 create mode 100644 modules/openat2
 create mode 100644 modules/openat2-tests
 create mode 100644 tests/test-openat2.c

diff --git a/ChangeLog b/ChangeLog
index bac7f4919f..b4519b0e32 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,5 +1,23 @@
 2025-10-27  Paul Eggert  <[email protected]>
 
+       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
diff --git a/MODULES.html.sh b/MODULES.html.sh
index e9d4cd7c86..e6826106c4 100755
--- a/MODULES.html.sh
+++ b/MODULES.html.sh
@@ -2894,6 +2894,7 @@ func_all_modules ()
   func_module getdtablesize
   func_module isapipe
   func_module openat-safer
+  func_module openat2
   func_module pipe-posix
   func_module pipe2
   func_module pipe2-safer
diff --git a/lib/fcntl.in.h b/lib/fcntl.in.h
index 843d36e013..3fbbf4b0e4 100644
--- a/lib/fcntl.in.h
+++ b/lib/fcntl.in.h
@@ -249,6 +249,46 @@ _GL_WARN_ON_USE (openat, "openat is not portable - "
 # 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.  */
 
diff --git a/lib/openat2.c b/lib/openat2.c
new file mode 100644
index 0000000000..ba5f579716
--- /dev/null
+++ b/lib/openat2.c
@@ -0,0 +1,569 @@
+/* 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;
+}
diff --git a/m4/fcntl_h.m4 b/m4/fcntl_h.m4
index 1c9f9cce02..6d6c8ff4de 100644
--- a/m4/fcntl_h.m4
+++ b/m4/fcntl_h.m4
@@ -1,5 +1,5 @@
 # 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,
@@ -26,7 +26,7 @@ AC_DEFUN_ONCE([gl_FCNTL_H]
   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])
@@ -53,6 +53,7 @@ AC_DEFUN([gl_FCNTL_H_REQUIRE_DEFAULTS]
     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])
@@ -66,6 +67,7 @@ AC_DEFUN([gl_FCNTL_H_DEFAULTS]
   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])
diff --git a/m4/openat2.m4 b/m4/openat2.m4
new file mode 100644
index 0000000000..891654f9c1
--- /dev/null
+++ b/m4/openat2.m4
@@ -0,0 +1,33 @@
+# 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>
+       ]])])
+])
diff --git a/modules/fcntl-h b/modules/fcntl-h
index 2f383ff0c8..2e8de83245 100644
--- a/modules/fcntl-h
+++ b/modules/fcntl-h
@@ -40,10 +40,12 @@ fcntl.h: fcntl.in.h $(top_builddir)/config.status 
$(CXXDEFS_H) $(ARG_NONNULL_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' \
diff --git a/modules/openat2 b/modules/openat2
new file mode 100644
index 0000000000..bf654f9c38
--- /dev/null
+++ b/modules/openat2
@@ -0,0 +1,49 @@
+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
diff --git a/modules/openat2-tests b/modules/openat2-tests
new file mode 100644
index 0000000000..88126f60ad
--- /dev/null
+++ b/modules/openat2-tests
@@ -0,0 +1,22 @@
+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@
diff --git a/tests/test-open.h b/tests/test-open.h
index 1a80af454a..f81799e57f 100644
--- a/tests/test-open.h
+++ b/tests/test-open.h
@@ -32,8 +32,9 @@
 # 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.  */
diff --git a/tests/test-openat2.c b/tests/test-openat2.c
new file mode 100644
index 0000000000..0ac7aca6f3
--- /dev/null
+++ b/tests/test-openat2.c
@@ -0,0 +1,527 @@
+/* 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;
+}
-- 
2.51.0



Reply via email to