Index: b/aclocal.m4
===================================================================
--- a/aclocal.m4
+++ b/aclocal.m4
@@ -1294,6 +1294,7 @@ m4_include([m4/open-cloexec.m4])
 m4_include([m4/open-slash.m4])
 m4_include([m4/open.m4])
 m4_include([m4/openat.m4])
+m4_include([m4/openat2.m4])
 m4_include([m4/opendir.m4])
 m4_include([m4/parse-datetime.m4])
 m4_include([m4/pathmax.m4])
Index: b/gnu/fcntl.in.h
===================================================================
--- a/gnu/fcntl.in.h
+++ b/gnu/fcntl.in.h
@@ -233,6 +233,46 @@ _GL_WARN_ON_USE (openat, "openat is not
 # 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.  */
 
@@ -435,6 +475,14 @@ _GL_WARN_ON_USE (openat, "openat is not
 # define AT_EACCESS 4
 #endif
 
+/* errno when openat+O_NOFOLLOW fails because the file is a symlink.  */
+#if defined __FreeBSD__ || defined __FreeBSD_kernel__ || defined __DragonFly__
+# define _GL_OPENAT_ESYMLINK EMLINK
+#elif defined __NetBSD__
+# define _GL_OPENAT_ESYMLINK EFTYPE
+#else
+# define _GL_OPENAT_ESYMLINK ELOOP
+#endif
 
 #endif /* _@GUARD_PREFIX@_FCNTL_H */
 #endif /* _@GUARD_PREFIX@_FCNTL_H */
Index: b/gnu/issymlinkat.c
===================================================================
--- /dev/null
+++ b/gnu/issymlinkat.c
@@ -0,0 +1,20 @@
+/* Test whether a file is a symbolic link.
+   Copyright (C) 2025 Free Software Foundation, Inc.
+
+   This file 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 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 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/>.  */
+
+#include <config.h>
+
+#define _GL_ISSYMLINKAT_INLINE _GL_EXTERN_INLINE
+#include "issymlink.h"
Index: b/gnu/issymlink.h
===================================================================
--- /dev/null
+++ b/gnu/issymlink.h
@@ -0,0 +1,103 @@
+/* Test whether a file is a symbolic link.
+   Copyright (C) 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/>.  */
+
+#ifndef _ISSYMLINK_H
+#define _ISSYMLINK_H
+
+/* This file uses _GL_ARG_NONNULL, _GL_INLINE.  */
+#ifndef _GL_ARG_NONNULL
+ #error "Please include config.h first."
+#endif
+
+#include <errno.h>
+#include <unistd.h> /* for readlink, readlinkat */
+
+
+_GL_INLINE_HEADER_BEGIN
+
+#ifndef _GL_ISSYMLINK_INLINE
+# define _GL_ISSYMLINK_INLINE _GL_INLINE
+#endif
+#ifndef _GL_ISSYMLINKAT_INLINE
+# define _GL_ISSYMLINKAT_INLINE _GL_INLINE
+#endif
+
+#if GNULIB_ISSYMLINK
+/* Tests whether FILENAME represents a symbolic link.
+   This function is more reliable than lstat() / fstatat() followed by S_ISLNK,
+   because it avoids possible EOVERFLOW errors.
+   Returns
+     1                      if FILENAME is a symbolic link,
+     0                      if FILENAME exists and is not a symbolic link,
+    -1 with errno set       if determination failed, in particular
+    -1 with errno = ENOENT or ENOTDIR  if FILENAME does not exist.  */
+# ifdef __cplusplus
+extern "C" {
+# endif
+_GL_ISSYMLINK_INLINE int issymlink (const char *filename)
+     _GL_ARG_NONNULL ((1));
+_GL_ISSYMLINK_INLINE int
+issymlink (const char *filename)
+{
+  char linkbuf[1];
+  if (readlink (filename, linkbuf, sizeof (linkbuf)) >= 0)
+    return 1;
+  if (errno == EINVAL)
+    return 0;
+  else
+    return -1;
+}
+# ifdef __cplusplus
+}
+# endif
+#endif
+
+#if GNULIB_ISSYMLINKAT
+/* Tests whether FILENAME represents a symbolic link.
+   This function is more reliable than lstat() / fstatat() followed by S_ISLNK,
+   because it avoids possible EOVERFLOW errors.
+   If FILENAME is a relative file name, it is interpreted as relative to the
+   directory referred to by FD (where FD = AT_FDCWD denotes the current
+   directory).
+   Returns
+     1                      if FILENAME is a symbolic link,
+     0                      if FILENAME exists and is not a symbolic link,
+    -1 with errno set       if determination failed, in particular
+    -1 with errno = ENOENT or ENOTDIR  if FILENAME does not exist.  */
+# ifdef __cplusplus
+extern "C" {
+# endif
+_GL_ISSYMLINKAT_INLINE int issymlinkat (int fd, const char *filename)
+     _GL_ARG_NONNULL ((2));
+_GL_ISSYMLINKAT_INLINE int
+issymlinkat (int fd, const char *filename)
+{
+  char linkbuf[1];
+  if (readlinkat (fd, filename, linkbuf, sizeof (linkbuf)) >= 0)
+    return 1;
+  if (errno == EINVAL)
+    return 0;
+  else
+    return -1;
+}
+# ifdef __cplusplus
+}
+# endif
+#endif
+
+_GL_INLINE_HEADER_END
+
+#endif /* _ISSYMLINK_H */
Index: b/gnu/Makefile.am
===================================================================
--- a/gnu/Makefile.am
+++ b/gnu/Makefile.am
@@ -68,8 +68,10 @@
 #  gitlog-to-changelog \
 #  hash \
 #  human \
+#  idx \
 #  inttostr \
 #  inttypes \
+#  issymlinkat \
 #  lchown \
 #  linkat \
 #  localcharset \
@@ -80,6 +82,7 @@
 #  modechange \
 #  obstack \
 #  openat \
+#  openat2 \
 #  parse-datetime \
 #  priv-set \
 #  progname \
@@ -738,10 +741,12 @@ fcntl.h: fcntl.in.h $(top_builddir)/conf
 	      -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' \
@@ -1288,6 +1293,14 @@ EXTRA_libgnu_a_SOURCES += isblank.c
 
 ## end   gnulib module isblank
 
+## begin gnulib module issymlinkat
+
+EXTRA_libgnu_a_SOURCES += issymlinkat.c
+
+EXTRA_DIST += issymlink.h
+
+## end   gnulib module issymlinkat
+
 ## begin gnulib module iswblank
 
 
@@ -1774,6 +1787,14 @@ EXTRA_DIST += fcntl--.h fcntl-safer.h
 
 ## end   gnulib module openat-safer
 
+## begin gnulib module openat2
+
+EXTRA_DIST += openat2.c
+
+EXTRA_libgnu_a_SOURCES += openat2.c
+
+## end   gnulib module openat2
+
 ## begin gnulib module opendir
 
 
Index: b/gnu/openat2.c
===================================================================
--- /dev/null
+++ b/gnu/openat2.c
@@ -0,0 +1,601 @@
+/* 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 "intprops.h"
+#include "idx.h"
+#include "verify.h"
+
+#include <errno.h>
+#include <limits.h>
+#include <stdbool.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#define ckd_add(r, a, b) ((bool) INT_ADD_WRAPV (a, b, r))
+#define ckd_sub(r, a, b) ((bool) INT_SUBTRACT_WRAPV (a, b, r))
+#define ckd_mul(r, a, b) ((bool) INT_MULTIPLY_WRAPV (a, b, r))
+
+#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 ENOTCAPABLE /* A FreeBSD error number.  */
+# define ENOTCAPABLE 0
+#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 = malloc (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 (h > 1)
+        {
+          h--;
+          if (ISSLASH (e[-h]))
+            break;
+        }
+
+      /* 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.  */
+          char eh = e[-h];
+          int subflags = ((!final
+                           ? O_PATHSEARCH | O_CLOEXEC | O_CLOFORK
+                           : flags)
+                          | O_NOFOLLOW | (eh ? O_DIRECTORY : 0));
+          e[-h] = '\0';
+          int subfd = openat (*fd, &e[-f], subflags, mode);
+
+          if (subfd < 0)
+            {
+              int openerr = negative_errno ();
+              if (! ((openerr == -_GL_OPENAT_ESYMLINK)
+                     | (!!(subflags & O_DIRECTORY) & (openerr == -ENOTDIR))))
+                return openerr;
+
+              /* Likely a symlink.
+                 Try to 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)
+                    {
+                      int readlinkerr = negative_errno ();
+                      return readlinkerr == -EINVAL ? -ENOTDIR : readlinkerr;
+                    }
+                  if (slinklen < slinksize)
+                    break;
+                }
+
+              if (maxlinks <= 0)
+                return -_GL_OPENAT_ESYMLINK;
+              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 -_GL_OPENAT_ESYMLINK;
+                }
+#endif
+
+              /* 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
+                {
+                  e[-h] = eh;
+                  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_CREAT)
+              & !!(how->flags & (O_DIRECTORY | O_TMPFILE)))
+           | (!!(how->flags & O_TMPFILE & ~O_DIRECTORY)
+              & ((how->flags & (O_ACCMODE | O_PATH)) != O_WRONLY)
+              & ((how->flags & (O_ACCMODE | O_PATH)) != O_RDWR))
+           | (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;
+
+      int fd;
+
+      /* For speed use a single openat if it suffices.  */
+      if (O_RESOLVE_BENEATH ? !(resolve & ~RESOLVE_BENEATH) : !resolve)
+        {
+          fd = openat (dfd, filename,
+                       flags | (resolve ? O_RESOLVE_BENEATH : 0), mode);
+
+          /* Return FD now unless openat failed with an errno that might
+             be an O_RESOLVE_BENEATH failure.  On platforms with
+             ENOTCAPABLE that is the errno; otherwise, this is macOS 15
+             where EACCES is the errno.  */
+          if (! (fd < 0 && resolve
+                 && errno == (ENOTCAPABLE ? ENOTCAPABLE : EACCES)))
+            return fd;
+        }
+
+      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;
+}
Index: b/gnu/xalloc.h
===================================================================
--- a/gnu/xalloc.h
+++ b/gnu/xalloc.h
@@ -21,6 +21,7 @@
 #include <stddef.h>
 #include <stdint.h>
 
+#include "idx.h"
 #include "xalloc-oversized.h"
 
 #ifndef _GL_INLINE_HEADER_BEGIN
@@ -59,6 +60,8 @@ void *xcalloc (size_t n, size_t s)
 void *xrealloc (void *p, size_t s)
       _GL_ATTRIBUTE_ALLOC_SIZE ((2));
 void *x2realloc (void *p, size_t *pn);
+void *xpalloc (void *pa, idx_t *nitems, idx_t nitems_incr_min,
+               ptrdiff_t nitems_max, idx_t item_size);
 void *xmemdup (void const *p, size_t s)
       _GL_ATTRIBUTE_ALLOC_SIZE ((2));
 char *xstrdup (char const *str)
Index: b/gnu/xmalloc.c
===================================================================
--- a/gnu/xmalloc.c
+++ b/gnu/xmalloc.c
@@ -21,6 +21,9 @@
 
 #include "xalloc.h"
 
+#include "intprops.h"
+#include "minmax.h"
+
 #include <stdlib.h>
 #include <string.h>
 
@@ -87,6 +90,66 @@ x2realloc (void *p, size_t *pn)
   return x2nrealloc (p, pn, 1);
 }
 
+/* Grow PA, which points to an array of *NITEMS items, and return the
+   location of the reallocated array, updating *NITEMS to reflect its
+   new size.  The new array will contain at least NITEMS_INCR_MIN more
+   items, but will not contain more than NITEMS_MAX items total.
+   ITEM_SIZE is the size of each item, in bytes.
+
+   ITEM_SIZE and NITEMS_INCR_MIN must be positive.  *NITEMS must be
+   nonnegative.  If NITEMS_MAX is -1, it is treated as if it were
+   infinity.
+
+   If PA is null, then allocate a new array instead of reallocating
+   the old one.
+
+   Thus, to grow an array A without saving its old contents, do
+   { free (A); A = xpalloc (NULL, &AITEMS, ...); }.  */
+
+void *
+xpalloc (void *pa, idx_t *nitems, idx_t nitems_incr_min,
+         ptrdiff_t nitems_max, idx_t item_size)
+{
+  idx_t n0 = *nitems;
+
+  /* The approximate size to use for initial small allocation
+     requests.  This is the largest "small" request for the GNU C
+     library malloc.  */
+  enum { DEFAULT_MXFAST = 64 * sizeof (size_t) / 4 };
+
+  /* If the array is tiny, grow it to about (but no greater than)
+     DEFAULT_MXFAST bytes.  Otherwise, grow it by about 50%.
+     Adjust the growth according to three constraints: NITEMS_INCR_MIN,
+     NITEMS_MAX, and what the C language can represent safely.  */
+
+  idx_t n, nbytes;
+  if (INT_ADD_WRAPV (n0, n0 >> 1, &n))
+    n = IDX_MAX;
+  if (0 <= nitems_max && nitems_max < n)
+    n = nitems_max;
+
+  idx_t adjusted_nbytes
+    = ((INT_MULTIPLY_WRAPV (n, item_size, &nbytes) || SIZE_MAX < nbytes)
+       ? MIN (IDX_MAX, SIZE_MAX)
+       : nbytes < DEFAULT_MXFAST ? DEFAULT_MXFAST : 0);
+  if (adjusted_nbytes)
+    {
+      n = adjusted_nbytes / item_size;
+      nbytes = adjusted_nbytes - adjusted_nbytes % item_size;
+    }
+
+  if (! pa)
+    *nitems = 0;
+  if (n - n0 < nitems_incr_min
+      && (INT_ADD_WRAPV (n0, nitems_incr_min, &n)
+          || (0 <= nitems_max && nitems_max < n)
+          || INT_MULTIPLY_WRAPV (n, item_size, &nbytes)))
+    xalloc_die ();
+  pa = xrealloc (pa, nbytes);
+  *nitems = n;
+  return pa;
+}
+
 /* Allocate N bytes of zeroed memory dynamically, with error checking.
    There's no need for xnzalloc (N, S), since it would be equivalent
    to xcalloc (N, S).  */
Index: b/m4/fcntl_h.m4
===================================================================
--- a/m4/fcntl_h.m4
+++ b/m4/fcntl_h.m4
@@ -23,7 +23,7 @@ AC_DEFUN([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])
 ])
 
 AC_DEFUN([gl_FCNTL_MODULE_INDICATOR],
@@ -42,12 +42,14 @@ AC_DEFUN([gl_FCNTL_H_DEFAULTS],
   GNULIB_NONBLOCKING=0;  AC_SUBST([GNULIB_NONBLOCKING])
   GNULIB_OPEN=0;         AC_SUBST([GNULIB_OPEN])
   GNULIB_OPENAT=0;       AC_SUBST([GNULIB_OPENAT])
+  GNULIB_OPENAT2=0;      AC_SUBST([GNULIB_OPENAT2])
   dnl Support Microsoft deprecated alias function names by default.
   GNULIB_MDA_CREAT=1;    AC_SUBST([GNULIB_MDA_CREAT])
   GNULIB_MDA_OPEN=1;     AC_SUBST([GNULIB_MDA_OPEN])
   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])
Index: b/m4/gnulib-comp.m4
===================================================================
--- a/m4/gnulib-comp.m4
+++ b/m4/gnulib-comp.m4
@@ -160,6 +160,7 @@ AC_DEFUN([gl_EARLY],
   # Code from module inttypes:
   # Code from module inttypes-incomplete:
   # Code from module isblank:
+  # Code from module issymlinkat:
   # Code from module iswblank:
   # Code from module iswdigit:
   # Code from module iswxdigit:
@@ -214,6 +215,7 @@ AC_DEFUN([gl_EARLY],
   # Code from module openat-die:
   # Code from module openat-h:
   # Code from module openat-safer:
+  # Code from module openat2:
   # Code from module opendir:
   # Code from module opendirat:
   # Code from module parse-datetime:
@@ -297,6 +299,7 @@ AC_DEFUN([gl_EARLY],
   # Code from module symlinkat:
   # Code from module sys_random:
   # Code from module sys_stat:
+  # Code from module sys_stat-h:
   # Code from module sys_time:
   # Code from module sys_types:
   # Code from module sysexits:
@@ -671,6 +674,7 @@ AC_DEFUN([gl_INIT],
   fi
   gl_MODULE_INDICATOR([isblank])
   gl_CTYPE_MODULE_INDICATOR([isblank])
+  gl_MODULE_INDICATOR([issymlinkat])
   gl_FUNC_ISWBLANK
   if test $HAVE_ISWCNTRL = 0 || test $REPLACE_ISWCNTRL = 1; then
     :
@@ -895,6 +899,12 @@ AC_DEFUN([gl_INIT],
   gl_FCNTL_MODULE_INDICATOR([openat])
   gl_OPENAT_SAFER
   gl_MODULE_INDICATOR([openat-safer])
+  gl_FUNC_OPENAT2
+if test $HAVE_OPENAT2 = 0; then
+    AC_LIBOBJ([openat2])
+    gl_PREREQ_OPENAT2
+  fi
+  gl_FCNTL_MODULE_INDICATOR([openat2])
   gl_FUNC_OPENDIR
   if test $HAVE_OPENDIR = 0 || test $REPLACE_OPENDIR = 1; then
     AC_LIBOBJ([opendir])
@@ -1575,6 +1585,8 @@ AC_DEFUN([gl_FILE_LIST], [
   lib/inttostr.h
   lib/inttypes.in.h
   lib/isblank.c
+  lib/issymlink.h
+  lib/issymlinkat.c
   lib/iswblank.c
   lib/iswdigit.c
   lib/iswxdigit.c
@@ -1652,6 +1664,7 @@ AC_DEFUN([gl_FILE_LIST], [
   lib/openat-safer.c
   lib/openat.c
   lib/openat.h
+  lib/openat2.c
   lib/opendir-safer.c
   lib/opendir.c
   lib/opendirat.c
@@ -1985,6 +1998,7 @@ AC_DEFUN([gl_FILE_LIST], [
   m4/open-slash.m4
   m4/open.m4
   m4/openat.m4
+  m4/openat2.m4
   m4/opendir.m4
   m4/parse-datetime.m4
   m4/pathmax.m4
Index: b/m4/openat2.m4
===================================================================
--- /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>
+       ]])])
+])
Index: b/src/common.h
===================================================================
--- a/src/common.h
+++ b/src/common.h
@@ -58,6 +58,7 @@
 #include <backupfile.h>
 #include <exclude.h>
 #include <full-write.h>
+#include <idx.h>
 #include <modechange.h>
 #include <quote.h>
 #include <safe-read.h>
@@ -403,7 +404,7 @@ GLOBAL ino_t ar_ino;
 
 /* Flags for reading, searching, and fstatatting files.  */
 GLOBAL int open_read_flags;
-GLOBAL int open_searchdir_flags;
+GLOBAL struct open_how open_searchdir_how;
 GLOBAL int fstatat_flags;
 
 GLOBAL int seek_option;
@@ -638,6 +639,7 @@ void assign_string_n (char **string, con
 #define ASSIGN_STRING_N(s,v) assign_string_n (s, v, sizeof (v))
 int unquote_string (char *str);
 char *zap_slashes (char *name);
+idx_t dotslashlen (char const *);
 char *normalize_filename (int cdidx, const char *name);
 void normalize_filename_x (char *name);
 void replace_prefix (char **pname, const char *samp, size_t slen,
@@ -713,10 +715,17 @@ int deref_stat (char const *name, struct
 size_t blocking_read (int fd, void *buf, size_t count);
 size_t blocking_write (int fd, void const *buf, size_t count);
 
+/* Not valid as the first argument to openat.
+   It is a negative integer not equal to AT_FDCWD.  */
+enum { BADFD = AT_FDCWD == -1 ? -2 : -1 };
+
 extern int chdir_current;
-extern int chdir_fd;
 int chdir_arg (char const *dir);
 void chdir_do (int dir);
+struct chdir_id { int err; dev_t st_dev; ino_t st_ino; } chdir_id (void);
+struct fdbase { int fd; char const *base; } fdbase (char const *);
+struct fdbase fdbase1 (char const *);
+void fdbase_clear (void);
 int chdir_count (void);
 
 void close_diag (char const *name);
Index: b/src/compare.c
===================================================================
--- a/src/compare.c
+++ b/src/compare.c
@@ -219,7 +219,9 @@ diff_file (void)
 	}
       else
 	{
-	  diff_handle = openat (chdir_fd, file_name, open_read_flags);
+	  struct fdbase f = fdbase (file_name);
+	  diff_handle = (f.fd == BADFD ? -1
+			 : openat (f.fd, f.base, open_read_flags));
 
 	  if (diff_handle < 0)
 	    {
@@ -240,8 +242,7 @@ diff_file (void)
 		  && stat_data.st_size != 0)
 		{
 		  struct timespec atime = get_stat_atime (&stat_data);
-		  if (set_file_atime (diff_handle, chdir_fd, file_name, atime)
-		      != 0)
+		  if (set_file_atime (diff_handle, f.fd, f.base, atime) != 0)
 		    utime_error (file_name);
 		}
 
@@ -275,9 +276,9 @@ diff_symlink (void)
   char buf[1024];
   size_t len = strlen (current_stat_info.link_name);
   char *linkbuf = len < sizeof buf ? buf : xmalloc (len + 1);
-
-  ssize_t status = readlinkat (chdir_fd, current_stat_info.file_name,
-			       linkbuf, len + 1);
+  struct fdbase f = fdbase (current_stat_info.file_name);
+  ssize_t status = (f.fd == BADFD ? -1
+		    : readlinkat (f.fd, f.base, linkbuf, len + 1));
 
   if (status < 0)
     {
@@ -434,7 +435,8 @@ diff_multivol (void)
     }
 
 
-  fd = openat (chdir_fd, current_stat_info.file_name, open_read_flags);
+  struct fdbase f = fdbase (current_stat_info.file_name);
+  fd = f.fd == BADFD ? -1 : openat (f.fd, f.base, open_read_flags);
 
   if (fd < 0)
     {
Index: b/src/create.c
===================================================================
--- a/src/create.c
+++ b/src/create.c
@@ -1386,8 +1386,10 @@ create_archive (void)
 		    {
 		      if (! st.orig_file_name)
 			{
-			  int fd = openat (chdir_fd, p->name,
-					   open_searchdir_flags);
+			  struct fdbase f = fdbase (p->name);
+			  int fd = (f.fd == BADFD ? -1
+				    : openat (f.fd, f.base,
+					      open_searchdir_how.flags));
 			  if (fd < 0)
 			    {
 			      file_removed_diag (p->name, !p->parent,
@@ -1590,9 +1592,18 @@ subfile_open (struct tar_stat_info const
       gettext ("");
     }
 
-  while ((fd = openat (dir ? dir->fd : chdir_fd, file, flags)) < 0
-	 && open_failure_recover (dir))
-    continue;
+  do
+    {
+      if (dir)
+	fd = openat (dir->fd, file, flags);
+      else
+	{
+	  struct fdbase f = fdbase (file);
+	  fd = f.fd == BADFD ? -1 : openat (f.fd, f.base, flags);
+	}
+    }
+  while (fd < 0 && open_failure_recover (dir));
+
   return fd;
 }
 
@@ -1606,7 +1617,7 @@ restore_parent_fd (struct tar_stat_info
   struct tar_stat_info *parent = st->parent;
   if (parent && ! parent->fd)
     {
-      int parentfd = openat (st->fd, "..", open_searchdir_flags);
+      int parentfd = openat (st->fd, "..", open_searchdir_how.flags);
       struct stat parentstat;
 
       if (parentfd < 0)
@@ -1621,8 +1632,9 @@ restore_parent_fd (struct tar_stat_info
 
       if (parentfd < 0)
 	{
-	  int origfd = openat (chdir_fd, parent->orig_file_name,
-			       open_searchdir_flags);
+	  struct fdbase f = fdbase (parent->orig_file_name);
+	  int origfd = (f.fd == BADFD ? -1
+			: openat (f.fd, f.base, open_searchdir_how.flags));
 	  if (0 <= origfd)
 	    {
 	      if (fstat (parentfd, &parentstat) == 0
@@ -1657,7 +1669,6 @@ dump_file0 (struct tar_stat_info *st, ch
   bool is_dir;
   struct tar_stat_info const *parent = st->parent;
   bool top_level = ! parent;
-  int parentfd = top_level ? chdir_fd : parent->fd;
   void (*diag) (char const *) = 0;
 
   if (interactive_option && !confirm ("add", p))
@@ -1669,12 +1680,15 @@ dump_file0 (struct tar_stat_info *st, ch
 
   transform_name (&st->file_name, XFORM_REGFILE);
 
-  if (parentfd < 0 && ! top_level)
+  struct fdbase f = (top_level ? fdbase (name)
+		     : (struct fdbase) { .fd = parent->fd, .base = name });
+  if (!top_level && parent->fd < 0)
     {
-      errno = - parentfd;
+      errno = - parent->fd;
       diag = open_diag;
     }
-  else if (fstatat (parentfd, name, &st->stat, fstatat_flags) != 0)
+  else if (f.fd == BADFD
+	   || fstatat (f.fd, f.base, &st->stat, fstatat_flags) != 0)
     diag = stat_diag;
   else if (file_dumpable_p (&st->stat))
     {
@@ -1749,9 +1763,9 @@ dump_file0 (struct tar_stat_info *st, ch
       bool ok;
       struct stat final_stat;
 
-      xattrs_acls_get (parentfd, name, st, 0, !is_dir);
-      xattrs_selinux_get (parentfd, name, st, fd);
-      xattrs_xattrs_get (parentfd, name, st, fd);
+      xattrs_acls_get (f.fd, f.base, st, 0, !is_dir);
+      xattrs_selinux_get (f.fd, f.base, st, fd);
+      xattrs_xattrs_get (f.fd, f.base, st, fd);
 
       if (is_dir)
 	{
@@ -1769,7 +1783,8 @@ dump_file0 (struct tar_stat_info *st, ch
 	  ok = dump_dir (st);
 
 	  fd = st->fd;
-	  parentfd = top_level ? chdir_fd : parent->fd;
+	  f = (top_level ? fdbase (name)
+	       : (struct fdbase) { .fd = parent->fd, .base = name });
 	}
       else
 	{
@@ -1810,13 +1825,13 @@ dump_file0 (struct tar_stat_info *st, ch
 	    }
 	  else if (fd == 0)
 	    {
-	      if (parentfd < 0 && ! top_level)
+	      if (!top_level && parent->fd < 0)
 		{
-		  errno = - parentfd;
+		  errno = - parent->fd;
 		  ok = false;
 		}
 	      else
-		ok = fstatat (parentfd, name, &final_stat, fstatat_flags) == 0;
+	        ok = fstatat (f.fd, f.base, &final_stat, fstatat_flags) == 0;
 	    }
 	  else
 	    ok = fstat (fd, &final_stat) == 0;
@@ -1840,7 +1855,7 @@ dump_file0 (struct tar_stat_info *st, ch
 	    }
 	  else if (atime_preserve_option == replace_atime_preserve
 		   && fd && (is_dir || original_size != 0)
-		   && set_file_atime (fd, parentfd, name, st->atime) != 0)
+		   && set_file_atime (fd, f.fd, f.base, st->atime) != 0)
 	    utime_error (p);
 	}
 
@@ -1853,7 +1868,7 @@ dump_file0 (struct tar_stat_info *st, ch
 #ifdef HAVE_READLINK
   else if (S_ISLNK (st->stat.st_mode))
     {
-      st->link_name = areadlinkat_with_size (parentfd, name, st->stat.st_size);
+      st->link_name = areadlinkat_with_size (f.fd, f.base, st->stat.st_size);
       if (!st->link_name)
 	{
 	  if (errno == ENOMEM)
@@ -1866,8 +1881,8 @@ dump_file0 (struct tar_stat_info *st, ch
 	  < strlen (st->link_name))
 	write_long_link (st);
 
-      xattrs_selinux_get (parentfd, name, st, 0);
-      xattrs_xattrs_get (parentfd, name, st, 0);
+      xattrs_selinux_get (f.fd, f.base, st, 0);
+      xattrs_xattrs_get (f.fd, f.base, st, 0);
 
       block_ordinal = current_block_ordinal ();
       st->stat.st_size = 0;	/* force 0 size on symlink */
@@ -1889,23 +1904,23 @@ dump_file0 (struct tar_stat_info *st, ch
   else if (S_ISCHR (st->stat.st_mode))
     {
       type = CHRTYPE;
-      xattrs_acls_get (parentfd, name, st, 0, true);
-      xattrs_selinux_get (parentfd, name, st, 0);
-      xattrs_xattrs_get (parentfd, name, st, 0);
+      xattrs_acls_get (f.fd, f.base, st, 0, true);
+      xattrs_selinux_get (f.fd, f.base, st, 0);
+      xattrs_xattrs_get (f.fd, f.base, st, 0);
     }
   else if (S_ISBLK (st->stat.st_mode))
     {
       type = BLKTYPE;
-      xattrs_acls_get (parentfd, name, st, 0, true);
-      xattrs_selinux_get (parentfd, name, st, 0);
-      xattrs_xattrs_get (parentfd, name, st, 0);
+      xattrs_acls_get (f.fd, f.base, st, 0, true);
+      xattrs_selinux_get (f.fd, f.base, st, 0);
+      xattrs_xattrs_get (f.fd, f.base, st, 0);
     }
   else if (S_ISFIFO (st->stat.st_mode))
     {
       type = FIFOTYPE;
-      xattrs_acls_get (parentfd, name, st, 0, true);
-      xattrs_selinux_get (parentfd, name, st, 0);
-      xattrs_xattrs_get (parentfd, name, st, 0);
+      xattrs_acls_get (f.fd, f.base, st, 0, true);
+      xattrs_selinux_get (f.fd, f.base, st, 0);
+      xattrs_xattrs_get (f.fd, f.base, st, 0);
     }
   else if (S_ISSOCK (st->stat.st_mode))
     {
Index: b/src/exclist.c
===================================================================
--- a/src/exclist.c
+++ b/src/exclist.c
@@ -77,7 +77,7 @@ info_attach_exclist (struct tar_stat_inf
     return;
   for (file = excfile_head; file; file = file->next)
     {
-      if (faccessat (dir ? dir->fd : chdir_fd, file->name, F_OK, 0) == 0)
+      if (faccessat (dir->fd, file->name, F_OK, 0) == 0)
 	{
 	  FILE *fp;
 	  struct exclude *ex = NULL;
@@ -176,12 +176,7 @@ excluded_name (char const *name, struct
 	    break;
 
 	  if (!rname)
-	    {
-	      rname = name;
-	      /* Skip leading ./ */
-	      while (*rname == '.' && ISSLASH (rname[1]))
-		rname += 2;
-	    }
+	    rname = name + dotslashlen (name);
 	  if ((result = excluded_file_name (ep->excluded, rname)))
 	    break;
 
Index: b/src/extract.c
===================================================================
--- a/src/extract.c
+++ b/src/extract.c
@@ -22,6 +22,7 @@
 #include <system.h>
 #include <quotearg.h>
 #include <errno.h>
+#include <issymlink.h>
 #include <priv-set.h>
 #include <root-uid.h>
 #include <utimens.h>
@@ -202,7 +203,8 @@ fd_i_chmod (int fd, char const *file, mo
       if (result == 0 || implemented (errno))
 	return result;
     }
-  return fchmodat (chdir_fd, file, mode, atflag);
+  struct fdbase f = fdbase (file);
+  return f.fd == BADFD ? -1 : fchmodat (f.fd, f.base, mode, atflag);
 }
 
 /* A version of fd_i_chmod which gracefully handles several common error
@@ -251,16 +253,18 @@ fd_chown (int fd, char const *file, uid_
       if (result == 0 || implemented (errno))
 	return result;
     }
-  return fchownat (chdir_fd, file, uid, gid, atflag);
+  struct fdbase f = fdbase (file);
+  return f.fd == BADFD ? -1 : fchownat (f.fd, f.base, uid, gid, atflag);
 }
 
 /* Use fstat if possible, fstatat otherwise.  */
 static int
 fd_stat (int fd, char const *file, struct stat *st, int atflag)
 {
-  return (0 <= fd
-	  ? fstat (fd, st)
-	  : fstatat (chdir_fd, file, st, atflag));
+  if (0 <= fd)
+    return fstat (fd, st);
+  struct fdbase f = fdbase (file);
+  return f.fd == BADFD ? -1 : fstatat (f.fd, f.base, st, atflag);
 }
 
 /* Set the mode for FILE_NAME to MODE.
@@ -358,7 +362,15 @@ set_stat (char const *file_name,
 	ts[0].tv_nsec = UTIME_OMIT;
       ts[1] = st->mtime;
 
-      if (fdutimensat (fd, chdir_fd, file_name, ts, atflag) == 0)
+      int r;
+      if (0 <= fd)
+	r = futimens (fd, ts);
+      if (fd < 0 || (r < 0 && errno == ENOSYS))
+	{
+	  struct fdbase f = fdbase (file_name);
+	  r = f.fd == BADFD ? -1 : utimensat (f.fd, f.base, ts, atflag);
+	}
+      if (r == 0)
 	{
 	  if (incremental_option)
 	    check_time (file_name, ts[0]);
@@ -478,8 +490,9 @@ delay_set_stat (char const *file_name, s
       if (data->interdir)
 	{
 	  struct stat real_st;
-	  if (fstatat (chdir_fd, data->file_name,
-		       &real_st, data->atflag) != 0)
+	  struct fdbase f = fdbase (data->file_name);
+	  if (f.fd == BADFD
+	      || fstatat (f.fd, f.base, &real_st, data->atflag) != 0)
 	    {
 	      stat_error (data->file_name);
 	    }
@@ -564,7 +577,8 @@ repair_delayed_set_stat (char const *dir
   for (data = delayed_set_stat_head; data; data = data->next)
     {
       struct stat st;
-      if (fstatat (chdir_fd, data->file_name, &st, data->atflag) != 0)
+      struct fdbase f = fdbase (data->file_name);
+      if (f.fd == BADFD || fstatat (f.fd, f.base, &st, data->atflag) != 0)
 	{
 	  stat_error (data->file_name);
 	  return;
@@ -678,7 +692,8 @@ make_directories (char *file_name)
       *cursor = '\0';		/* truncate the name there */
       desired_mode = MODE_RWX & ~ newdir_umask;
       mode = desired_mode | (we_are_root ? 0 : MODE_WXUSR);
-      status = mkdirat (chdir_fd, file_name, mode);
+      struct fdbase f = fdbase (file_name);
+      status = f.fd == BADFD ? -1 : mkdirat (f.fd, f.base, mode);
 
       if (status == 0)
 	{
@@ -720,8 +735,9 @@ make_directories (char *file_name)
      process may have created it, so check whether it exists now.  */
   *parent_end = '\0';
   struct stat st;
-  int stat_status = fstatat (chdir_fd, file_name, &st, 0);
-  if (!stat_status && !S_ISDIR (st.st_mode))
+  struct fdbase f = fdbase (file_name);
+  int stat_status = f.fd == BADFD ? -1 : fstatat (f.fd, f.base, &st, 0);
+  if (! (stat_status < 0 || S_ISDIR (st.st_mode)))
     stat_status = -1;
   if (stat_status)
     {
@@ -887,7 +903,8 @@ set_xattr (char const *file_name, struct
 
       for (;;)
         {
-          if (!mknodat (chdir_fd, file_name, mode ^ invert_permissions, 0))
+          struct fdbase f = fdbase (file_name);
+          if (f.fd != BADFD && !mknodat (f.fd, f.base, mode ^ invert_permissions, 0))
             {
               /* Successfully created file */
               xattrs_xattrs_set (st, file_name, typeflag, 0);
@@ -946,7 +963,8 @@ apply_nonancestor_delayed_set_stat (char
 
       if (check_for_renamed_directories)
 	{
-	  if (fstatat (chdir_fd, data->file_name, &st, data->atflag) != 0)
+	  struct fdbase f = fdbase (data->file_name);
+	  if (f.fd == BADFD || fstatat (f.fd, f.base, &st, data->atflag) != 0)
 	    {
 	      stat_error (data->file_name);
 	      skip_this_one = 1;
@@ -994,9 +1012,9 @@ apply_nonancestor_delayed_set_stat (char
 static bool
 is_directory_link (char const *file_name, struct stat *st)
 {
-  char buf[1];
-  return (0 <= readlinkat (chdir_fd, file_name, buf, sizeof buf)
-	  && fstatat (chdir_fd, file_name, st, 0) == 0
+  struct fdbase f = fdbase (file_name);
+  return (f.fd != BADFD && issymlinkat (f.fd, f.base)
+	  && fstatat (f.fd, f.base, st, 0) == 0
 	  && S_ISDIR (st->st_mode));
 }
 
@@ -1035,12 +1053,14 @@ extract_dir (char *file_name, int typefl
   /* Save 'root device' to avoid purging mount points. */
   if (one_file_system_option && root_device == 0)
     {
-      struct stat st;
-
-      if (fstatat (chdir_fd, ".", &st, 0) != 0)
+      struct chdir_id id = chdir_id ();
+      if (id.err)
+	{
+	  errno = id.err;
 	stat_diag (".");
+	}
       else
-	root_device = st.st_dev;
+	root_device = id.st_dev;
     }
 
   if (incremental_option)
@@ -1053,7 +1073,8 @@ extract_dir (char *file_name, int typefl
 
   for (;;)
     {
-      status = mkdirat (chdir_fd, file_name, mode);
+      struct fdbase f = fdbase (file_name);
+      status = f.fd == BADFD ? -1 : mkdirat (f.fd, f.base, mode);
       if (status == 0)
 	{
 	  current_mode = mode & ~ current_umask;
@@ -1187,21 +1208,20 @@ open_output_file (char const *file_name,
 	}
     }
 
+  struct fdbase f = fdbase (file_name);
+
   /* If O_NOFOLLOW is needed but does not work, check for a symlink
      separately.  There's a race condition, but that cannot be avoided
      on hosts lacking O_NOFOLLOW.  */
   if (! HAVE_WORKING_O_NOFOLLOW
-      && overwriting_old_files && ! dereference_option)
-    {
-      char buf[1];
-      if (0 <= readlinkat (chdir_fd, file_name, buf, sizeof buf))
+      && overwriting_old_files && ! dereference_option
+      && f.fd != BADFD && issymlinkat (f.fd, f.base))
 	{
 	  errno = ELOOP;
 	  return -1;
 	}
-    }
 
-  fd = openat (chdir_fd, file_name, openflag, mode);
+  fd = f.fd == BADFD ? -1 : openat (f.fd, f.base, openflag, mode);
   if (0 <= fd)
     {
       if (overwriting_old_files)
@@ -1369,7 +1389,8 @@ find_delayed_link_source (char const *na
   if (!delayed_link_head)
     return NULL;
   
-  if (fstatat (chdir_fd, name, &st, AT_SYMLINK_NOFOLLOW))
+  struct fdbase f = fdbase (name);
+  if (f.fd == BADFD || fstatat (f.fd, f.base, &st, AT_SYMLINK_NOFOLLOW) < 0)
     {
       if (errno != ENOENT)
 	stat_error (name);
@@ -1401,8 +1422,16 @@ create_placeholder_file (char *file_name
   int fd;
   struct stat st;
 
-  while ((fd = openat (chdir_fd, file_name, O_WRONLY | O_CREAT | O_EXCL, 0)) < 0)
+  for (;;)
     {
+      struct fdbase f = fdbase (file_name);
+      if (f.fd != BADFD)
+	{
+	  fd = openat (f.fd, f.base, O_WRONLY | O_CREAT | O_EXCL, 0);
+	  if (0 <= fd)
+	    break;
+	}
+
       if (errno == EEXIST && find_delayed_link_source (file_name))
 	{
 	  /* The placeholder file has already been created.  This means
@@ -1504,16 +1533,24 @@ extract_link (char *file_name, int typef
   
   do
     {
-      struct stat st1, st2;
-      int e;
-      int status = linkat (chdir_fd, link_name, chdir_fd, file_name, 0);
-      e = errno;
+      struct stat st, st1;
+      int status;
+
+      struct fdbase f = fdbase (file_name), f1;
+      if (f.fd == BADFD)
+       status = -1;
+      else
+       {
+         f1 = fdbase1 (link_name);
+         status = (f1.fd == BADFD ? -1
+                   : linkat (f1.fd, f1.base, f.fd, f.base, 0));
+       }
 
       if (status == 0)
 	{
 	  struct delayed_link *ds = delayed_link_head;
 	  if (ds
-	      && fstatat (chdir_fd, link_name, &st1, AT_SYMLINK_NOFOLLOW) == 0)
+             && fstatat (f1.fd, f1.base, &st1, AT_SYMLINK_NOFOLLOW) == 0)
 	    for (; ds; ds = ds->next)
 	      if (ds->change_dir == chdir_current
 		  && ds->dev == st1.st_dev
@@ -1530,14 +1567,14 @@ extract_link (char *file_name, int typef
 		}
 	  return 0;
 	}
-      else if ((e == EEXIST && strcmp (link_name, file_name) == 0)
-	       || ((fstatat (chdir_fd, link_name, &st1, AT_SYMLINK_NOFOLLOW)
-		    == 0)
-		   && (fstatat (chdir_fd, file_name, &st2, AT_SYMLINK_NOFOLLOW)
-		       == 0)
-		   && st1.st_dev == st2.st_dev
-		   && st1.st_ino == st2.st_ino))
-	return 0;
+      int e = errno;
+      if ((e == EEXIST && strcmp (link_name, file_name) == 0)
+         || (f.fd != BADFD && f1.fd != BADFD
+             && fstatat (f1.fd, f1.base, &st1, AT_SYMLINK_NOFOLLOW) == 0
+             && fstatat (f.fd, f.base, &st, AT_SYMLINK_NOFOLLOW) == 0
+          && st1.st_dev == st.st_dev
+          && st1.st_ino == st.st_ino))
+    return 0;
 
       errno = e;
     }
@@ -1565,7 +1602,10 @@ extract_symlink (char *file_name, int ty
 	  || contains_dot_dot (current_stat_info.link_name)))
     return create_placeholder_file (file_name, true, &interdir_made, NULL);
 
-  while (symlinkat (current_stat_info.link_name, chdir_fd, file_name) != 0)
+  for (struct fdbase f;
+       ((f = fdbase (file_name)).fd == BADFD
+	|| symlinkat (current_stat_info.link_name, f.fd, f.base) != 0);
+       )
     switch (maybe_recoverable (file_name, false, &interdir_made))
       {
       case RECOVER_OK:
@@ -1605,8 +1645,10 @@ extract_node (char *file_name, int typef
   mode_t mode = (current_stat_info.stat.st_mode & (MODE_RWX | S_IFBLK | S_IFCHR)
 		 & ~ (0 < same_owner_option ? S_IRWXG | S_IRWXO : 0));
 
-  while (mknodat (chdir_fd, file_name, mode, current_stat_info.stat.st_rdev)
-	 != 0)
+  for (struct fdbase f;
+       ((f = fdbase (file_name)).fd == BADFD
+	|| mknodat (f.fd, f.base, mode, current_stat_info.stat.st_rdev) != 0);
+       )
     switch (maybe_recoverable (file_name, false, &interdir_made))
       {
       case RECOVER_OK:
@@ -1635,7 +1677,10 @@ extract_fifo (char *file_name, int typef
   mode_t mode = (current_stat_info.stat.st_mode & MODE_RWX
 		 & ~ (0 < same_owner_option ? S_IRWXG | S_IRWXO : 0));
 
-  while (mkfifoat (chdir_fd, file_name, mode) != 0)
+  for (struct fdbase f;
+       ((f = fdbase (file_name)).fd == BADFD
+	|| mkfifoat (f.fd, f.base, mode) != 0);
+       )
     switch (maybe_recoverable (file_name, false, &interdir_made))
       {
       case RECOVER_OK:
@@ -1862,7 +1907,7 @@ apply_delayed_links (void)
   for (ds = delayed_link_head; ds; )
     {
       struct string_list *sources = ds->sources;
-      char const *valid_source = 0;
+  char const *valid_source = NULL;
 
       chdir_do (ds->change_dir);
 
@@ -1874,25 +1919,30 @@ apply_delayed_links (void)
 	  /* Make sure the placeholder file is still there.  If not,
 	     don't create a link, as the placeholder was probably
 	     removed by a later extraction.  */
-	  if (fstatat (chdir_fd, source, &st, AT_SYMLINK_NOFOLLOW) == 0
+      struct fdbase f = fdbase (source);
+      if (f.fd != BADFD && fstatat (f.fd, f.base, &st, AT_SYMLINK_NOFOLLOW) == 0      
 	      && st.st_dev == ds->dev
 	      && st.st_ino == ds->ino
 	      && timespec_cmp (get_stat_birthtime (&st), ds->birthtime) == 0)
 	    {
 	      /* Unlink the placeholder, then create a hard link if possible,
 		 a symbolic link otherwise.  */
-	      if (unlinkat (chdir_fd, source, 0) != 0)
+	  struct fdbase f1;
+	  if (unlinkat (f.fd, f.base, 0) != 0)
 		unlink_error (source);
 	      else if (valid_source
-		       && (linkat (chdir_fd, valid_source, chdir_fd, source, 0)
-			   == 0))
+		   && ((f1 = f.fd == BADFD ? f : fdbase1 (valid_source)).fd
+		       != BADFD)
+		   && linkat (f1.fd, f1.base, f.fd, f.base, 0) == 0)
 		;
 	      else if (!ds->is_symlink)
 		{
-		  if (linkat (chdir_fd, ds->target, chdir_fd, source, 0) != 0)
+	      f1 = f.fd == BADFD ? f : fdbase1 (ds->target);
+	      if (f1.fd == BADFD
+		  || linkat (f1.fd, f1.base, f.fd, f.base, 0) != 0)
 		    link_error (ds->target, source);
 		}
-	      else if (symlinkat (ds->target, chdir_fd, source) != 0)
+	  else if (symlinkat (ds->target, f.fd, f.base) != 0)
 		symlink_error (ds->target, source);
 	      else
 		{
@@ -1955,9 +2005,14 @@ extract_finish (void)
 bool
 rename_directory (char *src, char *dst)
 {
-  if (renameat (chdir_fd, src, chdir_fd, dst) == 0)
+  struct fdbase f1 = fdbase1 (src);
+  struct fdbase f = f1.fd == BADFD ? f1 : fdbase (dst);
+  if (f.fd != BADFD && renameat (f1.fd, f1.base, f.fd, f.base) == 0)
+    {
+      fdbase_clear ();
     fixup_delayed_set_stat (src, dst);
-  else
+    }
+  else if (f1.fd != BADFD)
     {
       int e = errno;
 
@@ -1966,8 +2021,13 @@ rename_directory (char *src, char *dst)
 	case ENOENT:
 	  if (make_directories (dst) == 0)
 	    {
-	      if (renameat (chdir_fd, src, chdir_fd, dst) == 0)
+	      f = fdbase (dst);
+	      if (f.fd != BADFD
+		  && renameat (f1.fd, f1.base, f.fd, f.base) == 0)
+		{
+		  fdbase_clear ();
 		return true;
+		}
 	      e = errno;
 	    }
 	  break;
Index: b/src/misc.c
===================================================================
--- a/src/misc.c
+++ b/src/misc.c
@@ -250,6 +250,30 @@ zap_slashes (char *name)
   return name;
 }
 
+/* The number of file name slashes at the start of the string F.  */
+static idx_t
+slashlen (char const *f)
+{
+  idx_t i = 0;
+  while (ISSLASH (f[i]))
+    i++;
+  return i;
+}
+
+/* The length of the longest initial prefix of F that consists
+   entirely of a sequence of '.'s each followed by one or more slashes.
+   This prefix acts like the working directory, i.e., the file name F
+   acts like "." if 0 < dotslashlen (F) == strlen (F), and acts like
+   &F[dotslashlen (F)] otherwise.  */
+idx_t
+dotslashlen (char const *f)
+{
+  idx_t i = 0;
+  while (f[i] == '.' && ISSLASH (f[i + 1]))
+    i += 2 + slashlen (&f[i + 2]);
+  return i;
+}
+
 /* Normalize FILE_NAME by removing redundant slashes and "."
    components, including redundant trailing slashes.
    Leave ".." alone, as it may be significant in the presence
@@ -260,8 +284,6 @@ void
 normalize_filename_x (char *file_name)
 {
   char *name = file_name + FILE_SYSTEM_PREFIX_LEN (file_name);
-  char *p;
-  char const *q;
   char c;
 
   /* Don't squeeze leading "//" to "/", on hosts where they're distinct.  */
@@ -269,16 +291,20 @@ normalize_filename_x (char *file_name)
 	   && ISSLASH (*name) && ISSLASH (name[1]) && ! ISSLASH (name[2]));
 
   /* Omit redundant leading "." components.  */
-  for (q = p = name; (*p = *q) == '.' && ISSLASH (q[1]); p += !*q)
-    for (q += 2; ISSLASH (*q); q++)
-      continue;
+  char *p = name;
+  char const *q = name + dotslashlen (name);
+
+  if (p < q && !*q)
+    q = ".";  /* NAME is nonempty and equivalent to ".".  */
 
   /* Copy components from Q to P, omitting redundant slashes and
      internal "."  components.  */
   while ((*p++ = c = *q++) != '\0')
     if (ISSLASH (c))
-      while (ISSLASH (q[*q == '.']))
-	q += (*q == '.') + 1;
+      {
+	q += slashlen (q);
+	q += dotslashlen (q);
+      }
 
   /* Omit redundant trailing "." component and slash.  */
   if (2 < p - name)
@@ -562,7 +588,8 @@ decode_timespec (char const *arg, char *
 static char *before_backup_name;
 static char *after_backup_name;
 
-/* Return 1 if FILE_NAME is obviously "." or "/".  */
+/* Return 1 if FILE_NAME must identify a working or root directory.
+   FILE_NAME should not be empty.  */
 bool
 must_be_dot_or_slash (char const *file_name)
 {
@@ -570,6 +597,7 @@ must_be_dot_or_slash (char const *file_n
 
   if (ISSLASH (file_name[0]))
     {
+      /* It must be a root directory if all components are "." or "..".  */
       for (;;)
 	if (ISSLASH (file_name[1]))
 	  file_name++;
@@ -581,31 +609,32 @@ must_be_dot_or_slash (char const *file_n
     }
   else
     {
-      while (file_name[0] == '.' && ISSLASH (file_name[1]))
-	{
-	  file_name += 2;
-	  while (ISSLASH (*file_name))
-	    file_name++;
-	}
-
-      return ! file_name[0] || (file_name[0] == '.' && ! file_name[1]);
+      /* It must be a working directory if it is "." or "",
+	 after skipping ^(\./+)* ERE.  */
+      file_name += dotslashlen (file_name);
+      return ! file_name[file_name[0] == '.'];
     }
 }
 
-/* Some implementations of rmdir let you remove '.' or '/'.
-   Report an error with errno set to zero for obvious cases of this;
-   otherwise call rmdir.  */
+/* Act like rmdir (FILENAME) relative to chdir_fd, i.e., like rmdir (F).
+   However, reject attempts to remove a root directory
+   even on systems that allow such a thing.
+   Also, do not try to change the removed directory's status later.  */
 static int
-safer_rmdir (const char *file_name)
+safer_rmdir (const char *file_name, struct fdbase f)
 {
-  if (must_be_dot_or_slash (file_name))
+  if (f.fd == BADFD)
+    return -1; /* Preserve errno.  */
+
+  if (IS_ABSOLUTE_FILE_NAME (f.base))
     {
-      errno = 0;
+      errno = EBUSY;
       return -1;
     }
 
-  if (unlinkat (chdir_fd, file_name, AT_REMOVEDIR) == 0)
+  if (f.fd != BADFD && unlinkat (f.fd, f.base, AT_REMOVEDIR) == 0)
     {
+      fdbase_clear ();
       remove_delayed_set_stat (file_name);
       return 0;
     }
@@ -627,10 +656,15 @@ remove_any_file (const char *file_name,
      non-directory.  */
   bool try_unlink_first = cannot_unlink_dir ();
 
+  struct fdbase f = fdbase (file_name);
+
   if (try_unlink_first)
     {
-      if (unlinkat (chdir_fd, file_name, 0) == 0)
+      if (f.fd != BADFD && unlinkat (f.fd, f.base, 0) == 0)
+	{
+	  fdbase_clear ();
 	return 1;
+	}
 
       /* POSIX 1003.1-2001 requires EPERM when attempting to unlink a
 	 directory without appropriate privileges, but many Linux
@@ -639,13 +673,16 @@ remove_any_file (const char *file_name,
 	return 0;
     }
 
-  if (safer_rmdir (file_name) == 0)
+  if (safer_rmdir (file_name, f) == 0)
     return 1;
 
   switch (errno)
     {
     case ENOTDIR:
-      return !try_unlink_first && unlinkat (chdir_fd, file_name, 0) == 0;
+      if (try_unlink_first || f.fd == BADFD || unlinkat (f.fd, f.base, 0) < 0)
+	return 0;
+      fdbase_clear ();
+      return 1;
 
     case 0:
     case EEXIST:
@@ -688,7 +725,7 @@ remove_any_file (const char *file_name,
 	      }
 
 	    free (directory);
-	    return safer_rmdir (file_name) == 0;
+	    return safer_rmdir (file_name, fdbase (file_name)) == 0;
 	  }
 	}
       break;
@@ -738,13 +775,27 @@ maybe_backup_file (const char *file_name
       && (S_ISBLK (file_stat.st_mode) || S_ISCHR (file_stat.st_mode)))
     return true;
 
-  after_backup_name = find_backup_file_name (chdir_fd, file_name, backup_type);
+  struct fdbase f = fdbase (file_name);
+  if (f.fd == BADFD)
+    {
+      open_error (file_name);
+      return false;
+    }
+  idx_t subdirlen = f.base - file_name;
+  after_backup_name = find_backup_file_name (f.fd, f.base, backup_type);
   if (! after_backup_name)
     xalloc_die ();
+  idx_t after_backup_namelen = strlen (after_backup_name);
+  after_backup_name = xrealloc (after_backup_name,
+				subdirlen + after_backup_namelen + 1);
+  memmove (after_backup_name + subdirlen, after_backup_name,
+	   after_backup_namelen + 1);
+  memcpy (after_backup_name, file_name, subdirlen);
 
-  if (renameat (chdir_fd, before_backup_name, chdir_fd, after_backup_name)
-      == 0)
+  if (renameat (f.fd, f.base, f.fd, &after_backup_name[subdirlen]) == 0)
     {
+      if (S_ISLNK (file_stat.st_mode))
+	fdbase_clear ();
       if (verbose_option)
 	fprintf (stdlis, _("Renaming %s to %s\n"),
 		 quote_n (0, before_backup_name),
@@ -770,8 +821,11 @@ undo_last_backup (void)
 {
   if (after_backup_name)
     {
-      if (renameat (chdir_fd, after_backup_name, chdir_fd, before_backup_name)
-	  != 0)
+      struct fdbase f = fdbase (before_backup_name);
+      if (f.fd == BADFD
+	  || (renameat (f.fd, &after_backup_name[f.base - before_backup_name],
+			f.fd, f.base)
+	      != 0))
 	{
 	  int e = errno;
 	  ERROR ((0, e, _("%s: Cannot rename to %s"),
@@ -792,7 +846,8 @@ undo_last_backup (void)
 int
 deref_stat (char const *name, struct stat *buf)
 {
-  return fstatat (chdir_fd, name, buf, fstatat_flags);
+  struct fdbase f = fdbase (name);
+  return f.fd == BADFD ? -1 : fstatat (f.fd, f.base, buf, fstatat_flags);
 }
 
 /* Read from FD into the buffer BUF with COUNT bytes.  Attempt to fill
@@ -871,16 +926,21 @@ struct wd
      the working directory.  If zero, the directory needs to be opened
      to be used.  */
   int fd;
+
+  /* If ID.err is zero, the directory's identity;
+     if positive, a failure indication with errno = ID.err;
+     if negative, no attempt has been made yet to get the identity.  */
+  struct chdir_id id;
 };
 
 /* A vector of chdir targets.  wd[0] is the initial working directory.  */
 static struct wd *wd;
 
 /* The number of working directories in the vector.  */
-static size_t wd_count;
+static idx_t wd_count;
 
 /* The allocated size of the vector.  */
-static size_t wd_alloc;
+static idx_t wd_alloc;
 
 /* The maximum number of chdir targets with open directories.
    Don't make it too large, as many operating systems have a small
@@ -890,10 +950,10 @@ enum { CHDIR_CACHE_SIZE = 16 };
 
 /* Indexes into WD of chdir targets with open file descriptors, sorted
    most-recently used first.  Zero indexes are unused.  */
-static int wdcache[CHDIR_CACHE_SIZE];
+static idx_t wdcache[CHDIR_CACHE_SIZE];
 
 /* Number of nonzero entries in WDCACHE.  */
-static size_t wdcache_count;
+static idx_t wdcache_count;
 
 int
 chdir_count (void)
@@ -903,33 +963,35 @@ chdir_count (void)
   return wd_count - 1;
 }
 
-/* DIR is the operand of a -C option; add it to vector of chdir targets,
-   and return the index of its location.  */
-int
-chdir_arg (char const *dir)
-{
-  if (wd_count == wd_alloc)
+/* Grow the WD table by at least one entry.  */
+static void
+grow_wd (void)
     {
-      if (wd_alloc == 0)
-	wd_alloc = 2;
-      wd = x2nrealloc (wd, &wd_alloc, sizeof *wd);
+  wd = xpalloc (wd, &wd_alloc, wd_alloc ? 1 : 2, -1, sizeof *wd);
 
       if (! wd_count)
 	{
 	  wd[wd_count].name = ".";
 	  wd[wd_count].abspath = NULL;
 	  wd[wd_count].fd = AT_FDCWD;
+      wd[wd_count].id.err = -1;
 	  wd_count++;
 	}
     }
 
+/* DIR is the operand of a -C option; add it to vector of chdir targets,
+   and return the index of its location.  */
+int
+chdir_arg (char const *dir)
+{
+  if (wd_count == wd_alloc)
+    grow_wd ();
+
   /* Optimize the common special case of the working directory,
      or the working directory as a prefix.  */
   if (dir[0])
     {
-      while (dir[0] == '.' && ISSLASH (dir[1]))
-	for (dir += 2;  ISSLASH (*dir);  dir++)
-	  continue;
+      dir += dotslashlen (dir);
       if (! dir[dir[0] == '.'])
 	return wd_count - 1;
     }
@@ -937,6 +999,7 @@ chdir_arg (char const *dir)
   wd[wd_count].name = dir;
   wd[wd_count].abspath = NULL;
   wd[wd_count].fd = 0;
+  wd[wd_count].id.err = -1;
   return wd_count++;
 }
 
@@ -947,7 +1010,7 @@ int chdir_current;
    similar locations for fstatat, etc.  This is an open file
    descriptor, or AT_FDCWD if the working directory is current.  It is
    valid until the next invocation of chdir_do.  */
-int chdir_fd = AT_FDCWD;
+static int chdir_fd = AT_FDCWD;
 
 /* Change to directory I, in a virtual way.  This does not actually
    invoke chdir; it merely sets chdir_fd to an int suitable as the
@@ -967,7 +1030,7 @@ chdir_do (int i)
 	  if (! IS_ABSOLUTE_FILE_NAME (curr->name))
 	    chdir_do (i - 1);
 	  fd = openat (chdir_fd, curr->name,
-		       open_searchdir_flags & ~ O_NOFOLLOW);
+		       open_searchdir_how.flags & ~O_NOFOLLOW);
 	  if (fd < 0)
 	    open_fatal (curr->name);
 
@@ -1009,6 +1072,186 @@ chdir_do (int i)
     }
 }
 
+/* Return the identity of the current directory.  */
+struct chdir_id
+chdir_id (void)
+{
+  if (!wd)
+    grow_wd ();
+
+  struct wd *curr = &wd[chdir_current];
+  if (curr->id.err < 0)
+    {
+      struct stat st;
+      curr->id = ((chdir_fd < 0 ? stat (".", &st) : fstat (chdir_fd, &st)) < 0
+		  ? (struct chdir_id) { .err = errno }
+		  : (struct chdir_id) { .st_dev = st.st_dev,
+		                        .st_ino = st.st_ino });
+    }
+  return curr->id;
+}
+
+/* Caches of recent calls to fdbase and fdbase1.  */
+static struct fdbase_cache
+{
+  /* Length of subdirectory name, which need not be null-terminated.
+     If the length is zero, no subdir is cached here:
+     SUBDIR (if nonnull) is merely a buffer available for use later,
+     and CHDIR_CURRENT and FD are irrelevant.  */
+  idx_t subdirlen;
+
+  /* Index of ancestor of this subdirectory.  */
+  idx_t chdir_current;
+
+  /* Buffer containing name of subdirectory relative to the ancestor.  */
+  char *subdir;
+
+  /* Number of bytes allocated for SUBDIR.  */
+  idx_t subdiralloc;
+
+  /* FD of subdirectory.  */
+  int fd;
+} fdbase_cache[2];
+
+/* Clear the fdbase cache.  Call this after any action that might
+   invalidate the cache.  Such actions include removing or renaming
+   directories or symlinks to directories.  Call this if in doubt,
+   e.g., if it is not known whether a removed directory entry is a
+   symlink to a directory.  */
+void
+fdbase_clear (void)
+{
+  for (int i = 0; i < 2; i++)
+    {
+      struct fdbase_cache *c = &fdbase_cache[i];
+      if (c->subdirlen)
+	{
+	  if (0 <= c->fd)
+	    close (c->fd);
+	  c->subdirlen = 0;
+	}
+    }
+}
+
+/* Starting from the directory FD, open a subdirectory SUBDIR for search.
+   If extracting or diffing and --absolute-names (-P) is not in effect,
+   do not let the subdirectory escape FD, i.e., the subdirectory must
+   be at or under FD in the directory hierarchy.  */
+static int
+open_subdir (int fd, char const *subdir)
+{
+  return openat2 (fd, subdir, &open_searchdir_how, sizeof open_searchdir_how);
+}
+
+/* Return an fd open to FILE_NAME's parent directory,
+   along with the base name of FILE_NAME.
+   Use the alternate cache if ALTERNATE, the main cache otherwise.
+   If FILE_NAME is relative, it is relative to chdir_fd.
+   Return AT_FDCWD if FILE_NAME is relative to the working directory.
+   Return BADFD (setting errno) on failure.  */
+static struct fdbase
+fdbase_opendir (char const *file_name, bool alternate)
+{
+  char const *name = file_name;
+
+  /* Skip past leading "./"s,
+     but not past the last "./" if that ends the name.  */
+  idx_t dslen = dotslashlen (name);
+  if (dslen)
+    {
+      name += dslen;
+      if (!*name)
+	for (name--; *--name != '.'; )
+	  continue;
+    }
+
+  /* For files immediately under CHDIR_FD, and for root directories,
+     just use CHDIR_FD and NAME.  */
+  char const *base = last_component (name);
+  idx_t subdirlen = base - name;
+  if (!subdirlen | !*base)
+    return (struct fdbase) { .fd = chdir_fd, .base = name };
+
+  struct fdbase_cache *c = &fdbase_cache[alternate];
+  int fd = c->fd;
+  bool submatch = (0 < c->subdirlen && c->subdirlen <= subdirlen
+		   && c->chdir_current == chdir_current
+		   && !ISSLASH (name[c->subdirlen])
+		   && memcmp (c->subdir, name, c->subdirlen) == 0);
+
+  if (! (submatch && c->subdirlen == subdirlen))
+    {
+      /* Copy the new directory's name into the cache.  */
+      char *subdir = c->subdir;
+      if (c->subdiralloc <= subdirlen)
+	c->subdir = subdir = xpalloc (subdir, &c->subdiralloc,
+				      subdirlen - c->subdiralloc + 1, -1, 1);
+      char *p = mempcpy (subdir, name, subdirlen);
+      *p = '\0';
+
+      if (submatch && c->subdirlen < subdirlen
+	  && !ISSLASH (subdir[c->subdirlen]))
+	{
+	  /* The new directory is a subdirectory of the old,
+	     so open relative to FD rather than to chdir_fd.  */
+	  int subfd = open_subdir (fd, &subdir[c->subdirlen]);
+	  if (subfd < 0)
+	    {
+	      /* Keep the old directory cached and report open failure,
+		 unless EMFILE means it's possible that falling
+		 through to close the old directory would mean we
+		 could successfully retry from the chdir_fd level.
+	         When reporting failure, there is no need to
+	         null-terminate the old directory, since the code does
+	         not assume null termination.  */
+	      if (errno != EMFILE)
+		return (struct fdbase) { .fd = BADFD, .base = base };
+	    }
+	  else
+	    {
+	      /* Replace the old directory with the new one.  */
+	      close (fd);
+	      c->fd = subfd;
+	      c->subdirlen = subdirlen;
+	      return (struct fdbase) { .fd = subfd, .base = base };
+	    }
+	}
+
+      /* Remove any old directory info,
+	 and add new info if the new directory can be opened.  */
+      if (0 < c->subdirlen)
+	close (fd);
+      fd = open_subdir (chdir_fd, c->subdir);
+      if (fd < 0)
+	{
+	  if (BADFD != -1 && fd < 0)
+	    fd = BADFD;
+	  c->subdirlen = 0;
+	}
+      else
+	{
+	  c->chdir_current = chdir_current;
+	  c->fd = fd;
+	  c->subdirlen = subdirlen;
+	}
+    }
+
+  return (struct fdbase) { .fd = fd, .base = base };
+}
+
+struct fdbase
+fdbase (char const *name)
+{
+  return fdbase_opendir (name, false);
+}
+
+struct fdbase
+fdbase1 (char const *name)
+{
+  return fdbase_opendir (name, true);
+}
+
+
 const char *
 tar_dirname (void)
 {
@@ -1291,7 +1534,9 @@ tar_savedir (const char *name, int must_
 {
   char *ret = NULL;
   DIR *dir = NULL;
-  int fd = openat (chdir_fd, name, open_read_flags | O_DIRECTORY);
+  struct fdbase f = fdbase (name);
+  int fd = (f.fd == BADFD ? -1
+	    : openat (f.fd, f.base, open_read_flags | O_DIRECTORY));
   if (fd < 0)
     {
       if (!must_exist && errno == ENOENT)
Index: b/src/names.c
===================================================================
--- a/src/names.c
+++ b/src/names.c
@@ -1782,8 +1782,9 @@ collect_and_sort_names (void)
 	}
       if (S_ISDIR (st.stat.st_mode))
 	{
-	  int dir_fd = openat (chdir_fd, name->name,
-			       open_read_flags | O_DIRECTORY);
+	  struct fdbase f = fdbase (name->name);
+	  int dir_fd = (f.fd == BADFD ? -1
+			: openat (f.fd, f.base, open_read_flags | O_DIRECTORY));
 	  if (dir_fd < 0)
 	    open_diag (name->name);
 	  else
Index: b/src/tar.c
===================================================================
--- a/src/tar.c
+++ b/src/tar.c
@@ -2597,16 +2597,29 @@ decode_options (int argc, char **argv)
   if (recursive_unlink_option)
     old_files_option = UNLINK_FIRST_OLD_FILES;
 
-  /* Flags for accessing files to be read from or copied into.  POSIX says
-     O_NONBLOCK has unspecified effect on most types of files, but in
-     practice it never harms and sometimes helps.  */
+  /* Flags for accessing files to be read from, searched, or statted.  */
   {
-    int base_open_flags =
-      (O_BINARY | O_CLOEXEC | O_NOCTTY | O_NONBLOCK
-       | (dereference_option ? 0 : O_NOFOLLOW)
-       | (atime_preserve_option == system_atime_preserve ? O_NOATIME : 0));
-    open_read_flags = O_RDONLY | base_open_flags;
-    open_searchdir_flags = O_SEARCH | O_DIRECTORY | base_open_flags;
+    int noatime_flag = (atime_preserve_option == system_atime_preserve
+			? O_NOATIME : 0);
+    int nofollow_flag = dereference_option ? 0 : O_NOFOLLOW;
+
+    /* POSIX says O_NONBLOCK has unspecified effect on most types of
+       files, but in practice it harms only with O_PATH and sometimes
+       helps otherwise.  */
+    open_read_flags = (O_RDONLY | O_BINARY | O_CLOEXEC | O_NOCTTY | O_NONBLOCK
+		       | noatime_flag | nofollow_flag);
+
+#if defined O_PATH && O_SEARCH == O_RDONLY
+    int search_flags = O_PATH; /* openat2 rejects O_PATH | O_NOATIME.  */
+#else
+    int search_flags = O_SEARCH | noatime_flag;
+#endif
+    open_searchdir_how.flags = (search_flags | nofollow_flag
+				| O_BINARY | O_CLOEXEC | O_DIRECTORY);
+    if (!absolute_names_option
+	&& (subcommand_option == EXTRACT_SUBCOMMAND
+	    || subcommand_option == DIFF_SUBCOMMAND))
+      open_searchdir_how.resolve = RESOLVE_BENEATH;
   }
   fstatat_flags = dereference_option ? 0 : AT_SYMLINK_NOFOLLOW;
 
Index: b/src/unlink.c
===================================================================
--- a/src/unlink.c
+++ b/src/unlink.c
@@ -117,7 +117,10 @@ flush_deferred_unlinks (bool force)
 	      else
 		fname = p->file_name;
 
-	      if (unlinkat (chdir_fd, fname, AT_REMOVEDIR) != 0)
+	      struct fdbase f = fdbase (fname);
+	      if (f.fd != BADFD && unlinkat (f.fd, f.base, AT_REMOVEDIR) == 0)
+		fdbase_clear ();
+	      else
 		{
 		  switch (errno)
 		    {
@@ -143,7 +146,10 @@ flush_deferred_unlinks (bool force)
 	    }
 	  else
 	    {
-	      if (unlinkat (chdir_fd, p->file_name, 0) != 0 && errno != ENOENT)
+	      struct fdbase f = fdbase (p->file_name);
+	      if (f.fd != BADFD && unlinkat (f.fd, f.base, 0) == 0)
+		fdbase_clear ();
+	      else if (errno != ENOENT)
 		unlink_error (p->file_name);
 	    }
 	  dunlink_reclaim (p);
@@ -178,11 +184,12 @@ flush_deferred_unlinks (bool force)
 	  else
 	    fname = p->file_name;
 
-	  if (unlinkat (chdir_fd, fname, AT_REMOVEDIR) != 0)
-	    {
-	      if (errno != ENOENT)
+	  struct fdbase f = fdbase (fname);
+	  if (f.fd != BADFD && unlinkat (f.fd, f.base, AT_REMOVEDIR) == 0)
+	    fdbase_clear ();
+	  else if (errno != ENOENT)
 		rmdir_error (fname);
-	    }
+
 	  dunlink_reclaim (p);
 	  dunlink_count--;
 	  p = next;
Index: b/src/update.c
===================================================================
--- a/src/update.c
+++ b/src/update.c
@@ -47,7 +47,8 @@ char *output_start;
 static void
 append_file (char *file_name)
 {
-  int handle = openat (chdir_fd, file_name, O_RDONLY | O_BINARY);
+  struct fdbase f = fdbase (file_name);
+  int handle = f.fd == BADFD ? -1 : openat (f.fd, f.base, O_RDONLY | O_BINARY);
   struct stat stat_data;
 
   if (handle < 0)
Index: b/src/xattrs.c
===================================================================
--- a/src/xattrs.c
+++ b/src/xattrs.c
@@ -219,7 +219,8 @@ xattrs__acls_set (struct tar_stat_info c
       /* No "default" IEEE 1003.1e ACL set for directory.  At this moment,
          FILE_NAME may already have inherited default acls from parent
          directory;  clean them up. */
-      if (acl_delete_def_file_at (chdir_fd, file_name))
+      struct fdbase f1 = fdbase (file_name);
+      if (f1.fd == BADFD || acl_delete_def_file_at (f1.fd, f1.base) < 0)      
         WARNOPT (WARN_XATTR_WRITE,
                 (0, errno,
                  _("acl_delete_def_file_at: Cannot drop default POSIX ACLs "
@@ -236,7 +237,8 @@ xattrs__acls_set (struct tar_stat_info c
       return;
     }
 
-  if (acl_set_file_at (chdir_fd, file_name, type, acl) == -1)
+  struct fdbase f = fdbase (file_name);
+  if (f.fd == BADFD || acl_set_file_at (f.fd, f.base, type, acl) < 0)
     /* warn even if filesystem does not support acls */
     WARNOPT (WARN_XATTR_WRITE,
 	     (0, errno,
@@ -540,14 +542,17 @@ xattrs__fd_set (struct tar_stat_info con
   if (ptr)
     {
       const char *sysname = "setxattrat";
-      int ret = -1;
+      int ret;
+      struct fdbase f = fdbase (file_name);
 
-      if (typeflag != SYMTYPE)
-        ret = setxattrat (chdir_fd, file_name, attr, ptr, len, 0);
+      if (f.fd == BADFD)
+	ret = -1;
+      else if (typeflag != SYMTYPE)
+	ret = setxattrat (f.fd, f.base, attr, ptr, len, 0);
       else
         {
           sysname = "lsetxattr";
-          ret = lsetxattrat (chdir_fd, file_name, attr, ptr, len, 0);
+	  ret = lsetxattrat (f.fd, f.base, attr, ptr, len, 0);
         }
 
       if (ret == -1)
@@ -601,14 +606,17 @@ xattrs_selinux_set (struct tar_stat_info
       if (!st->cntx_name)
         return;
 
-      if (typeflag != SYMTYPE)
+      struct fdbase f = fdbase (file_name);
+      if (f.fd == BADFD)
+	ret = -1;
+      else if (typeflag != SYMTYPE)
         {
-          ret = setfileconat (chdir_fd, file_name, st->cntx_name);
+	  ret = setfileconat (f.fd, f.base, st->cntx_name);
           sysname = "setfileconat";
         }
       else
         {
-          ret = lsetfileconat (chdir_fd, file_name, st->cntx_name);
+	  ret = lsetfileconat (f.fd, f.base, st->cntx_name);
           sysname = "lsetfileconat";
         }
 
Index: b/tests/extrac31.at
===================================================================
--- /dev/null
+++ b/tests/extrac31.at
@@ -0,0 +1,55 @@
+# Test suite for GNU tar.                             -*- Autotest -*-
+# Copyright 2025 Free Software Foundation, Inc.
+#
+# This file is part of GNU tar.
+#
+# GNU tar 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.
+#
+# GNU tar 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 <http://www.gnu.org/licenses/>.
+
+AT_SETUP([extracting untrusted incremental])
+AT_KEYWORDS([extract extrac31 --absolute-names])
+
+
+AT_TAR_CHECK([
+
+# Extraction should not escape the extraction directory
+# even when extracting multiple times to the same directory.
+(umask 022 && mkdir -p dira/sub dirb/sym dirb/sub/sym ext victimdir victimexp)
+ln -s .. dira/sub/dotdot
+ln -s ../sub dira/sub/dot
+ln -s dotdot/sub dira/sub/anotherdot
+ln -s ../victimdir dira/sym
+ln -s dotdot/../victimdir dira/sub/sym
+echo b1 >dirb/sym/file1
+echo b2 >dirb/sub/sym/file2
+echo v >victimdir/expected
+echo v >victimdir/file1
+echo v >victimdir/file2
+cp victimdir/* victimexp
+tar -cf a.tar -C dira sub sym
+tar -cf b.tar -C dirb sym/file1 sub/sym/file2
+tar -xf a.tar -C ext
+echo status1=$?
+tar -xf b.tar -C ext
+echo status2=$?
+diff victimdir victimexp
+],
+[],
+[status1=0
+status2=2
+],
+[tar: sym/file1: Cannot open: Invalid cross-device link
+tar: sub/sym/file2: Cannot open: Invalid cross-device link
+tar: Exiting with failure status due to previous errors
+])
+AT_CLEANUP
Index: b/tests/Makefile.am
===================================================================
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -123,6 +123,7 @@ TESTSUITE_AT = \
  extrac22.at\
  extrac23.at\
  extrac24.at\
+ extrac31.at\
  filerem01.at\
  filerem02.at\
  dirrem01.at\
Index: b/tests/testsuite.at
===================================================================
--- a/tests/testsuite.at
+++ b/tests/testsuite.at
@@ -345,6 +345,7 @@ m4_include([extrac21.at])
 m4_include([extrac22.at])
 m4_include([extrac23.at])
 m4_include([extrac24.at])
+m4_include([extrac31.at])
 
 m4_include([backup01.at])
 
