[packages/tar] Rel 2. Backport post-1.35 extraction and archive-parsing fixes.

arekm arekm at pld-linux.org
Tue Oct 20 22:50:32 CEST 2026


commit adaef9d884f75901a38e4d96e12c2f47b9b16714
Author: Arkadiusz Miśkiewicz <arekm at maven.pl>
Date:   Tue Oct 20 22:49:16 2026 +0200

    Rel 2. Backport post-1.35 extraction and archive-parsing fixes.
    
    CVE: CVE-2025-45582, CVE-2026-5704, CVE-2026-18508, CVE-2026-18477

 tar-CVE-2025-45582.patch                 | 2416 ++++++++++++++++++++++++++++++
 tar-CVE-2026-18477.patch                 |  412 +++++
 tar-CVE-2026-18508.patch                 |  775 ++++++++++
 tar-CVE-2026-5704.patch                  |  472 ++++++
 tar-acl-namespace.patch                  |   87 ++
 tar-deref-extract-over-symlink.patch     |   98 ++
 tar-dumpdir-missing-terminator.patch     |   86 ++
 tar-fix-extract-dot-diagnostic.patch     |  137 ++
 tar-fix-restore-parent-fd.patch          |   15 +
 tar-missing-exclude-tests.patch          |  136 ++
 tar-one-top-level-must-be-relative.patch |   49 +
 tar.spec                                 |   39 +-
 12 files changed, 4719 insertions(+), 3 deletions(-)
---
diff --git a/tar.spec b/tar.spec
index d050ffc..11efbd6 100644
--- a/tar.spec
+++ b/tar.spec
@@ -1,6 +1,6 @@
 #
 # Conditional build:
-%bcond_with	tests	# perform make check
+%bcond_without	tests	# perform make check
 
 Summary:	A GNU file archiving program
 Summary(de.UTF-8):	GNU-Magnetband-Archivierprogramm (tar)
@@ -11,7 +11,7 @@ Summary(pt_BR.UTF-8):	GNU Tape Archiver (tar)
 Summary(tr.UTF-8):	Yaygın kullanılan yedekleyici
 Name:		tar
 Version:	1.35
-Release:	1
+Release:	2
 Epoch:		1
 License:	GPL v3+
 Group:		Applications/Archiving
@@ -21,14 +21,32 @@ Source1:	http://www.mif.pg.gda.pl/homepages/ankry/man-PLD/%{name}-non-english-ma
 # Source1-md5:	4e4b1655fe42c27a4eb5d7bcd82e74ac
 Patch0:		%{name}-info.patch
 Patch1:		%{name}-zero-block.patch
+Patch2:		%{name}-fix-extract-dot-diagnostic.patch
+Patch3:		%{name}-CVE-2025-45582.patch
+Patch4:		%{name}-one-top-level-must-be-relative.patch
+Patch5:		%{name}-acl-namespace.patch
+Patch6:		%{name}-CVE-2026-5704.patch
+Patch7:		%{name}-CVE-2026-18508.patch
+Patch8:		%{name}-CVE-2026-18477.patch
+Patch9:		%{name}-missing-exclude-tests.patch
+Patch10:	%{name}-dumpdir-missing-terminator.patch
+Patch11:	%{name}-deref-extract-over-symlink.patch
+Patch12:	%{name}-fix-restore-parent-fd.patch
 URL:		http://www.gnu.org/software/tar/tar.html
+BuildRequires:	acl-devel
 BuildRequires:	autoconf >= 2.71
 BuildRequires:	automake >= 1:1.11
 BuildRequires:	bison
 BuildRequires:	gettext-tools >= 0.21
 BuildRequires:	help2man
+BuildRequires:	libselinux-devel
 BuildRequires:	sed >= 4.0
 BuildRequires:	texinfo
+%if %{with tests}
+BuildRequires:	acl
+BuildRequires:	attr
+BuildRequires:	bash
+%endif
 Conflicts:	amanda-client < 2.5.2
 BuildRoot:	%{tmpdir}/%{name}-%{version}-root-%(id -u -n)
 
@@ -140,6 +158,17 @@ z pakietu dump.
 %setup -q
 %patch -P0 -p1
 %patch -P1 -p1
+%patch -P2 -p1
+%patch -P3 -p1
+%patch -P4 -p1
+%patch -P5 -p1
+%patch -P6 -p1
+%patch -P7 -p1
+%patch -P8 -p1
+%patch -P9 -p1
+%patch -P10 -p1
+%patch -P11 -p1
+%patch -P12 -p1
 
 %{__rm} po/stamp-po
 
@@ -154,7 +183,10 @@ z pakietu dump.
 
 %{__make}
 
-%{?with_tests:%{__make} check}
+%if %{with tests}
+# upstream test scripts use bash-only redirections
+%{__make} check SHELL=/bin/bash
+%endif
 
 %install
 rm -rf $RPM_BUILD_ROOT
@@ -162,6 +194,7 @@ install -d $RPM_BUILD_ROOT{/usr/bin,%{_mandir}/man1}
 
 %{__make} install \
 	DESTDIR=$RPM_BUILD_ROOT
+%{__rm} $RPM_BUILD_ROOT%{_infodir}/dir
 
 ln -sf %{_bindir}/tar $RPM_BUILD_ROOT/usr/bin/gtar
 
diff --git a/tar-CVE-2025-45582.patch b/tar-CVE-2025-45582.patch
new file mode 100644
index 0000000..29be200
--- /dev/null
+++ b/tar-CVE-2025-45582.patch
@@ -0,0 +1,2416 @@
+# upstream extraction jail (75b03fdf and the fdbase/chdir series):
+# keep extracted paths inside the extraction directory (CVE-2025-45582)
+diff --git a/doc/tar.texi b/doc/tar.texi
+index 2057e1b..282cd0a 100644
+--- a/doc/tar.texi
++++ b/doc/tar.texi
+@@ -12947,26 +12947,31 @@ when you later extract from the archive you will get incorrect data.
+ When @command{tar} extracts from an archive, by default it writes into
+ files relative to the working directory.  If the archive was generated
+ by an untrusted user, that user therefore can write into any file
+-under the working directory.  If the working directory contains a
+-symbolic link to another directory, the untrusted user can also write
+-into any file under the referenced directory.  When extracting from an
++under the working directory.  When extracting from an
+ untrusted archive, it is therefore good practice to create an empty
+-directory and run @command{tar} in that directory.
+-
+-When extracting from two or more untrusted archives, each one should
+-be extracted independently, into different empty directories.
+-Otherwise, the first archive could create a symbolic link into an area
+-outside the working directory, and the second one could follow the
+-link and overwrite data that is not under the working directory.  For
+-example, when restoring from a series of incremental dumps, the
+-archives should have been created by a trusted process, as otherwise
+-the incremental restores might alter data outside the working
+-directory.
++directory and run @command{tar} in that directory.  You can use the
++ at option{--directory} (@option{-C}) option to specify the working
++directory (@pxref{directory}).
++
++When extracting from an archive, @command{tar} rejects attempts to
++modify files outside the working directory.
++For example, if a symbolic link points outside the working directory,
++ at command{tar} refuses to follow the link, regardless of whether the
++symbolic link existed before @command{tar} was run.
++Therefore, when extracting from two or more untrusted archives,
++each one can be extracted in turn, into the same initially-empty directory.
++Even if an earlier archive creates a symbolic link that
++points outside the working directory,
++ at command{tar} will reject any later attempts to follow that symbolic link.
++However, this safety mechanism applies only to @command{tar} itself:
++it does not apply to other programs you may run later, which will
++ordinarily follow symbolic links even if they escape the working directory.
+ 
+ If you use the @option{--absolute-names} (@option{-P}) option when
+ extracting, @command{tar} respects any file names in the archive, even
+-file names that begin with @file{/} or contain @file{..}.  As this
+-lets the archive overwrite any file in your system that you can write,
++file names that begin with @file{/}, contain @file{..}, or that follow
++a symbolic link to escape the extraction directory.  As this lets the
++archive overwrite any file in your system that you can write,
+ the @option{--absolute-names} (@option{-P}) option should be used only
+ for trusted archives.
+ 
+@@ -13036,7 +13041,7 @@ Protect archives at least as much as you protect any of the files
+ being archived.
+ 
+ @item
+-Extract from an untrusted archive only into an otherwise-empty
++Extract from untrusted archives only into an otherwise-empty
+ directory.  This directory and its parent should be accessible only to
+ trusted users.  For example:
+ 
+@@ -13049,8 +13054,6 @@ $ @kbd{tar -xvf /archives/got-it-off-the-net.tar.gz}
+ @end group
+ @end example
+ 
+-As a corollary, do not do an incremental restore from an untrusted archive.
+-
+ @item
+ Do not let untrusted users access files extracted from untrusted
+ archives without checking first for problems such as setuid programs.
+diff --git a/gnu/Makefile.am b/gnu/Makefile.am
+index 0e2fc49..69d0cae 100644
+--- a/gnu/Makefile.am
++++ b/gnu/Makefile.am
+@@ -86,6 +86,7 @@
+ #  modechange \
+ #  obstack \
+ #  openat \
++#  openat2 \
+ #  parse-datetime \
+ #  priv-set \
+ #  progname \
+@@ -935,10 +936,12 @@ fcntl.h: fcntl.in.h $(top_builddir)/config.status $(CXXDEFS_H) $(ARG_NONNULL_H)
+ 	      -e 's/@''GNULIB_NONBLOCKING''@/$(GL_GNULIB_NONBLOCKING)/g' \
+ 	      -e 's/@''GNULIB_OPEN''@/$(GL_GNULIB_OPEN)/g' \
+ 	      -e 's/@''GNULIB_OPENAT''@/$(GL_GNULIB_OPENAT)/g' \
++	      -e 's/@''GNULIB_OPENAT2''@/$(GL_GNULIB_OPENAT2)/g' \
+ 	      -e 's/@''GNULIB_MDA_CREAT''@/$(GL_GNULIB_MDA_CREAT)/g' \
+ 	      -e 's/@''GNULIB_MDA_OPEN''@/$(GL_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' \
+@@ -2060,6 +2063,14 @@ EXTRA_DIST += fcntl--.h fcntl-safer.h
+ 
+ ## end   gnulib module openat-safer
+ 
++## begin gnulib module openat2
++
++if GL_COND_OBJ_OPENAT2
++libgnu_a_SOURCES += openat2.c
++endif
++
++## end   gnulib module openat2
++
+ ## begin gnulib module opendir
+ 
+ if GL_COND_OBJ_OPENDIR
+diff --git a/gnu/fcntl.in.h b/gnu/fcntl.in.h
+index d0bb85f..e08d2f7 100644
+--- a/gnu/fcntl.in.h
++++ b/gnu/fcntl.in.h
+@@ -238,6 +238,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.  */
+ 
+@@ -445,6 +485,15 @@ _GL_WARN_ON_USE (openat, "openat is not portable - "
+ # define AT_NO_AUTOMOUNT 0
+ #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 /* _ at GUARD_PREFIX@_FCNTL_H */
+ #endif /* _ at GUARD_PREFIX@_FCNTL_H */
+ #endif
+diff --git a/gnu/openat2.c b/gnu/openat2.c
+new file mode 100644
+index 0000000..bfa2a8e
+--- /dev/null
++++ b/gnu/openat2.c
+@@ -0,0 +1,596 @@
++/* 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 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 = 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 (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;
++}
+diff --git a/m4/fcntl_h.m4 b/m4/fcntl_h.m4
+index 68f4e64..30317a6 100644
+--- a/m4/fcntl_h.m4
++++ b/m4/fcntl_h.m4
+@@ -1,6 +1,6 @@
+-# serial 20
+-# Configure fcntl.h.
+-dnl Copyright (C) 2006-2007, 2009-2023 Free Software Foundation, Inc.
++# fcntl_h.m4
++# serial 21
++dnl Copyright (C) 2006-2007, 2009-2025 Free Software Foundation, Inc.
+ dnl This file is free software; the Free Software Foundation
+ dnl gives unlimited permission to copy and/or distribute it,
+ dnl with or without modifications, as long as this notice is preserved.
+@@ -23,7 +23,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])
+@@ -50,6 +50,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])
+@@ -63,6 +64,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/gnulib-comp.m4 b/m4/gnulib-comp.m4
+index 33c295d..0a4f371 100644
+--- a/m4/gnulib-comp.m4
++++ b/m4/gnulib-comp.m4
+@@ -237,6 +237,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:
+@@ -1064,6 +1065,13 @@ AC_DEFUN([gl_INIT],
+   gl_FCNTL_MODULE_INDICATOR([openat])
+   gl_OPENAT_SAFER
+   gl_MODULE_INDICATOR([openat-safer])
++  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])
+   gl_FUNC_OPENDIR
+   gl_CONDITIONAL([GL_COND_OBJ_OPENDIR],
+                  [test $HAVE_OPENDIR = 0 || test $REPLACE_OPENDIR = 1])
+@@ -2020,6 +2028,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
+@@ -2386,6 +2395,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
+diff --git a/m4/openat2.m4 b/m4/openat2.m4
+new file mode 100644
+index 0000000..891654f
+--- /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/src/common.h b/src/common.h
+index 9451e13..b4a5821 100644
+--- a/src/common.h
++++ b/src/common.h
+@@ -401,7 +401,7 @@ GLOBAL struct stat archive_stat;
+ 
+ /* 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;
+@@ -644,6 +644,7 @@ void assign_string_n (char **string, const char *value, size_t n);
+ #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,
+@@ -722,10 +723,17 @@ int deref_stat (char const *name, struct stat *buf);
+ 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);
+diff --git a/src/compare.c b/src/compare.c
+index 4f9824c..a8e1260 100644
+--- a/src/compare.c
++++ b/src/compare.c
+@@ -218,7 +218,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)
+ 	    {
+@@ -239,7 +241,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)
++		  if (set_file_atime (diff_handle, f.fd, f.base, atime)
+ 		      != 0)
+ 		    utime_error (file_name);
+ 		}
+@@ -274,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)
+     {
+@@ -403,7 +405,7 @@ static void
+ diff_multivol (void)
+ {
+   struct stat stat_data;
+-  int fd, status;
++  int status;
+   off_t offset;
+ 
+   if (current_stat_info.had_trailing_slash)
+@@ -433,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);
++  int fd = f.fd == BADFD ? -1 : openat (f.fd, f.base, open_read_flags);
+ 
+   if (fd < 0)
+     {
+diff --git a/src/create.c b/src/create.c
+index d20178c..845b194 100644
+--- a/src/create.c
++++ b/src/create.c
+@@ -1376,8 +1376,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,
+@@ -1579,9 +1581,18 @@ subfile_open (struct tar_stat_info const *dir, char const *file, int flags)
+       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;
+ }
+ 
+@@ -1595,7 +1606,7 @@ restore_parent_fd (struct tar_stat_info const *st)
+   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)
+@@ -1610,8 +1621,9 @@ restore_parent_fd (struct tar_stat_info const *st)
+ 
+       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
+@@ -1644,7 +1656,6 @@ dump_file0 (struct tar_stat_info *st, char const *name, char const *p)
+   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))
+@@ -1656,12 +1667,24 @@ dump_file0 (struct tar_stat_info *st, char const *name, char const *p)
+ 
+   transform_name (&st->file_name, XFORM_REGFILE);
+ 
+-  if (parentfd < 0 && ! top_level)
++  struct fdbase f;
++  if (top_level)
++    f = fdbase (name);
++  else
++    {
++      /* Avoid a compound literal here, to work around a bug
++	 in Oracle Developer Studio 12.6 (sparc64).  */
++      f.fd = parent->fd;
++      f.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))
+     {
+@@ -1737,9 +1760,9 @@ dump_file0 (struct tar_stat_info *st, char const *name, char const *p)
+       bool ok;
+       struct stat st2;
+ 
+-      xattrs_acls_get (parentfd, name, st, !is_dir);
+-      xattrs_selinux_get (parentfd, name, st, fd);
+-      xattrs_xattrs_get (parentfd, name, st, fd);
++      xattrs_acls_get (f.fd, f.base, st, !is_dir);
++      xattrs_selinux_get (f.fd, f.base, st, fd);
++      xattrs_xattrs_get (f.fd, f.base, st, fd);
+ 
+       if (is_dir)
+ 	{
+@@ -1757,7 +1780,15 @@ dump_file0 (struct tar_stat_info *st, char const *name, char const *p)
+ 	  ok = dump_dir (st);
+ 
+ 	  fd = st->fd;
+-	  parentfd = top_level ? chdir_fd : parent->fd;
++	  if (top_level)
++	    f = fdbase (name);
++	  else
++	    {
++	      /* Avoid a compound literal here, to work around a bug
++		 in Oracle Developer Studio 12.6 (sparc64).  */
++	      f.fd = parent->fd;
++	      f.base = name;
++	    }
+ 	}
+       else
+ 	{
+@@ -1798,9 +1829,9 @@ dump_file0 (struct tar_stat_info *st, char const *name, char const *p)
+ 	    }
+ 	  else if (fd == 0)
+ 	    {
+-	      if (parentfd < 0 && ! top_level)
++	      if (!top_level && parent->fd < 0)
+ 		{
+-		  errno = - parentfd;
++		  errno = - parent->fd;
+ 		  ok = false;
+ 		}
+ 	    }
+@@ -1851,7 +1882,7 @@ dump_file0 (struct tar_stat_info *st, char const *name, char const *p)
+ 	    }
+ 	  else if (atime_preserve_option == replace_atime_preserve
+ 		   && timespec_cmp (st->atime, get_stat_atime (&st2)) != 0
+-		   && set_file_atime (fd, parentfd, name, st->atime) != 0)
++		   && set_file_atime (fd, f.fd, f.base, st->atime) != 0)
+ 	    utime_error (p);
+ 	}
+ 
+@@ -1864,7 +1895,7 @@ dump_file0 (struct tar_stat_info *st, char const *name, char const *p)
+ #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)
+@@ -1877,8 +1908,8 @@ dump_file0 (struct tar_stat_info *st, char const *name, char const *p)
+ 	  < 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 */
+@@ -1900,23 +1931,23 @@ dump_file0 (struct tar_stat_info *st, char const *name, char const *p)
+   else if (S_ISCHR (st->stat.st_mode))
+     {
+       type = CHRTYPE;
+-      xattrs_acls_get (parentfd, name, st, true);
+-      xattrs_selinux_get (parentfd, name, st, 0);
+-      xattrs_xattrs_get (parentfd, name, st, 0);
++      xattrs_acls_get (f.fd, f.base, st, 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, true);
+-      xattrs_selinux_get (parentfd, name, st, 0);
+-      xattrs_xattrs_get (parentfd, name, st, 0);
++      xattrs_acls_get (f.fd, f.base, st, 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, true);
+-      xattrs_selinux_get (parentfd, name, st, 0);
+-      xattrs_xattrs_get (parentfd, name, st, 0);
++      xattrs_acls_get (f.fd, f.base, st, 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))
+     {
+diff --git a/src/exclist.c b/src/exclist.c
+index 92b814f..8886272 100644
+--- a/src/exclist.c
++++ b/src/exclist.c
+@@ -79,7 +79,7 @@ info_attach_exclist (struct tar_stat_info *dir)
+     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;
+@@ -178,12 +178,7 @@ excluded_name (char const *name, struct tar_stat_info *st)
+ 	    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;
+ 
+diff --git a/src/extract.c b/src/extract.c
+index 3e3932b..ae03190 100644
+--- a/src/extract.c
++++ b/src/extract.c
+@@ -246,7 +246,8 @@ fd_i_chmod (int fd, char const *file, mode_t mode, int atflag)
+       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
+@@ -295,16 +296,18 @@ fd_chown (int fd, char const *file, uid_t uid, gid_t gid, int atflag)
+       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.
+@@ -402,7 +405,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]);
+@@ -522,8 +533,9 @@ delay_set_stat (char const *file_name, struct tar_stat_info const *st,
+       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);
+ 	    }
+@@ -604,7 +616,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;
+@@ -718,7 +731,8 @@ make_directories (char *file_name, bool *interdir_made)
+       *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)
+ 	{
+@@ -761,8 +775,9 @@ make_directories (char *file_name, bool *interdir_made)
+      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)
+     {
+@@ -918,7 +933,8 @@ set_xattr (char const *file_name, struct tar_stat_info const *st,
+ #ifdef HAVE_XATTRS
+   if ((xattrs_option > 0) && st->xattr_map.xm_size)
+     {
+-      int r = mknodat (chdir_fd, file_name, mode, 0);
++      struct fdbase f = fdbase (file_name);
++      int r = f.fd == BADFD ? -1 : mknodat (f.fd, f.base, mode, 0);
+       if (r < 0)
+ 	return r;
+       xattrs_xattrs_set (st, file_name, typeflag, 0);
+@@ -962,7 +978,8 @@ apply_nonancestor_delayed_set_stat (char const *file_name, bool after_links)
+ 
+       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;
+@@ -1010,8 +1027,9 @@ 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 && 0 <= readlinkat (f.fd, f.base, buf, sizeof buf)
++	  && fstatat (f.fd, f.base, st, 0) == 0
+ 	  && S_ISDIR (st->st_mode));
+ }
+ 

+@@ -1050,12 +1068,14 @@ extract_dir (char *file_name, int typeflag)
+   /* 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)
+-	stat_diag (".");
++      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)
+@@ -1068,7 +1088,8 @@ extract_dir (char *file_name, int typeflag)
+ 
+   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;
+@@ -1200,21 +1221,24 @@ open_output_file (char const *file_name, int typeflag, mode_t mode,
+ 	}
+     }
+ 
++  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)
++      && overwriting_old_files && ! dereference_option
++      && f.fd != BADFD)
+     {
+       char buf[1];
+-      if (0 <= readlinkat (chdir_fd, file_name, buf, sizeof buf))
++      if (0 <= readlinkat (f.fd, f.base, buf, sizeof buf))
+ 	{
+ 	  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 (openflag & O_EXCL)
+@@ -1380,7 +1404,8 @@ find_delayed_link_source (char const *name)
+   if (!delayed_link_table)
+     return false;
+ 
+-  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))
+     {
+       if (errno != ENOENT)
+ 	stat_error (name);
+@@ -1406,8 +1431,16 @@ create_placeholder_file (char *file_name, bool is_symlink, bool *interdir_made)
+   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
+@@ -1429,7 +1462,7 @@ create_placeholder_file (char *file_name, bool is_symlink, bool *interdir_made)
+ 	  open_error (file_name);
+ 	  return -1;
+ 	}
+-      }
++    }
+ 
+   if (fstat (fd, &st) != 0)
+     {
+@@ -1506,15 +1539,23 @@ extract_link (char *file_name, MAYBE_UNUSED int typeflag)
+ 
+   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)
+ 	{
+ 	  if (delayed_link_table
+-	      && fstatat (chdir_fd, link_name, &st1, AT_SYMLINK_NOFOLLOW) == 0)
++	      && fstatat (f1.fd, f1.base, &st1, AT_SYMLINK_NOFOLLOW) == 0)
+ 	    {
+ 	      struct delayed_link dl1;
+ 	      dl1.ino = st1.st_ino;
+@@ -1534,15 +1575,15 @@ extract_link (char *file_name, MAYBE_UNUSED int typeflag)
+ 
+ 	  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;
+     }
+   while ((rc = maybe_recoverable (file_name, false, &interdir_made))
+@@ -1569,7 +1610,10 @@ extract_symlink (char *file_name, MAYBE_UNUSED int typeflag)
+ 	  || contains_dot_dot (current_stat_info.link_name)))
+     return create_placeholder_file (file_name, true, &interdir_made);
+ 
+-  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:
+@@ -1609,8 +1653,10 @@ extract_node (char *file_name, int typeflag)
+   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:
+@@ -1639,7 +1685,10 @@ extract_fifo (char *file_name, int typeflag)
+   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 +1911,7 @@ static void
+ apply_delayed_link (struct delayed_link *ds)
+ {
+   struct string_list *sources = ds->sources;
+-  char const *valid_source = 0;
++  char const *valid_source = NULL;
+ 
+   chdir_do (ds->change_dir);
+ 
+@@ -1874,25 +1923,30 @@ apply_delayed_link (struct delayed_link *ds)
+       /* 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
+ 	  && BIRTHTIME_EQ (get_stat_birthtime (&st), ds->birthtime))
+ 	{
+ 	  /* 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
+ 	    {
+@@ -1968,9 +2022,14 @@ extract_finish (void)
+ bool
+ rename_directory (char *src, char *dst)
+ {
+-  if (renameat (chdir_fd, src, chdir_fd, dst) == 0)
+-    fixup_delayed_set_stat (src, dst);
+-  else
++  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 if (f1.fd != BADFD)
+     {
+       int e = errno;
+ 
+@@ -1979,8 +2038,13 @@ rename_directory (char *src, char *dst)
+ 	case ENOENT:
+ 	  if (make_directories (dst, NULL) == 0)
+ 	    {
+-	      if (renameat (chdir_fd, src, chdir_fd, dst) == 0)
+-		return true;
++	      f = fdbase (dst);
++	      if (f.fd != BADFD
++		  && renameat (f1.fd, f1.base, f.fd, f.base) == 0)
++		{
++		  fdbase_clear ();
++		  return true;
++		}
+ 	      e = errno;
+ 	    }
+ 	  break;
+diff --git a/src/misc.c b/src/misc.c
+index 73c0464..0e874eb 100644
+--- a/src/misc.c
++++ b/src/misc.c
+@@ -269,6 +269,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
+@@ -279,8 +303,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.  */
+@@ -288,16 +310,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)
+@@ -581,7 +607,8 @@ decode_timespec (char const *arg, char **arg_lim, bool parse_fraction)
+ 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)
+ {
+@@ -589,6 +616,7 @@ must_be_dot_or_slash (char const *file_name)
+ 
+   if (ISSLASH (file_name[0]))
+     {
++      /* It must be a root directory if all components are "." or "..".  */
+       for (;;)
+ 	if (ISSLASH (file_name[1]))
+ 	  file_name++;
+@@ -600,31 +628,32 @@ must_be_dot_or_slash (char const *file_name)
+     }
+   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;
+     }
+@@ -646,10 +675,15 @@ remove_any_file (const char *file_name, enum remove_option option)
+      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)
+-	return 1;
++      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
+@@ -658,13 +692,16 @@ remove_any_file (const char *file_name, enum remove_option option)
+ 	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:
+@@ -705,7 +742,7 @@ remove_any_file (const char *file_name, enum remove_option option)
+ 	      }
+ 
+ 	    free (directory);
+-	    return safer_rmdir (file_name) == 0;
++	    return safer_rmdir (file_name, fdbase (file_name)) == 0;
+ 	  }
+ 	}
+       break;
+@@ -755,13 +792,27 @@ maybe_backup_file (const char *file_name, bool this_is_the_archive)
+       && (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 ();
+-
+-  if (renameat (chdir_fd, before_backup_name, chdir_fd, after_backup_name)
+-      == 0)
++  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 (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),
+@@ -787,8 +838,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"),
+@@ -809,7 +863,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
+@@ -890,6 +945,11 @@ 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.  */
+@@ -922,33 +982,37 @@ chdir_count (void)
+   return wd_count - 1;
+ }
+ 
++/* 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);
++
++  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)
+-    {
+-      if (wd_alloc == 0)
+-	wd_alloc = 2;
+-      wd = x2nrealloc (wd, &wd_alloc, sizeof *wd);
+-
+-      if (! wd_count)
+-	{
+-	  wd[wd_count].name = ".";
+-	  wd[wd_count].abspath = NULL;
+-	  wd[wd_count].fd = AT_FDCWD;
+-	  wd_count++;
+-	}
+-    }
++    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;
+     }
+@@ -956,6 +1020,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++;
+ }
+ 
+@@ -966,7 +1031,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
+@@ -986,7 +1051,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);
+ 
+@@ -1027,6 +1092,186 @@ chdir_do (int i)
+       chdir_fd = fd;
+     }
+ }
++
++/* 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)
+@@ -1310,7 +1555,9 @@ tar_savedir (const char *name, int must_exist)
+ {
+   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)
+diff --git a/src/names.c b/src/names.c
+index d302737..54fd696 100644
+--- a/src/names.c
++++ b/src/names.c
+@@ -1797,8 +1797,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
+diff --git a/src/tar.c b/src/tar.c
+index 7efb003..272c63b 100644
+--- a/src/tar.c
++++ b/src/tar.c
+@@ -2614,16 +2614,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;
+ 
+diff --git a/src/unlink.c b/src/unlink.c
+index 704e42b..33d2ccd 100644
+--- 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)
+-		rmdir_error (fname);
+-	    }
++	  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;
+diff --git a/src/update.c b/src/update.c
+index dc5e5ac..2bc3820 100644
+--- a/src/update.c
++++ b/src/update.c
+@@ -49,7 +49,8 @@ static bool acting_as_filter;
+ 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);
+ 
+   if (handle < 0)
+     {
+diff --git a/src/xattrs.c b/src/xattrs.c
+index 86a7e59..92698ea 100644
+--- a/src/xattrs.c
++++ b/src/xattrs.c
+@@ -299,7 +299,8 @@ xattrs__acls_set (struct tar_stat_info const *st,
+       /* 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))
+         WARNOPT (WARN_XATTR_WRITE,
+                 (0, errno,
+                  _("acl_delete_def_file_at: Cannot drop default POSIX ACLs "
+@@ -316,7 +317,8 @@ xattrs__acls_set (struct tar_stat_info const *st,
+       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) == -1)
+     /* warn even if filesystem does not support acls */
+     WARNOPT (WARN_XATTR_WRITE,
+ 	     (0, errno,
+@@ -619,13 +621,16 @@ xattrs__fd_set (char const *file_name, char typeflag,
+     {
+       const char *sysname = "setxattrat";
+       int ret = -1;
++      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)
+@@ -679,14 +684,17 @@ xattrs_selinux_set (struct tar_stat_info const *st,
+       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";
+         }
+ 
+diff --git a/tests/Makefile.am b/tests/Makefile.am
+index ea5b66e..2c4aa3f 100644
+--- a/tests/Makefile.am
++++ b/tests/Makefile.am
+@@ -126,6 +126,7 @@ TESTSUITE_AT = \
+  extrac24.at\
+  extrac25.at\
+  extrac27.at\
++ extrac31.at\
+  filerem01.at\
+  filerem02.at\
+  dirrem01.at\
+diff --git a/tests/extrac31.at b/tests/extrac31.at
+new file mode 100644
+index 0000000..5cc68a4
+--- /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
+diff --git a/tests/testsuite.at b/tests/testsuite.at
+index 5cb87c7..963836c 100644
+--- a/tests/testsuite.at
++++ b/tests/testsuite.at
+@@ -350,6 +350,7 @@ m4_include([extrac23.at])
+ m4_include([extrac24.at])
+ m4_include([extrac25.at])
+ m4_include([extrac27.at])
++m4_include([extrac31.at])
+ 
+ m4_include([backup01.at])
+ 
diff --git a/tar-CVE-2026-18477.patch b/tar-CVE-2026-18477.patch
new file mode 100644
index 0000000..812813e
--- /dev/null
+++ b/tar-CVE-2026-18477.patch
@@ -0,0 +1,412 @@
+# upstream 0714d2f0, d479b2cc, b17665b2: strip '/' from incremental
+# dumpdir 'X' rename templates unless -P (CVE-2026-18477)
+diff --git a/lib/Makefile.am b/lib/Makefile.am
+index 38645b2..45152c4 100644
+--- a/lib/Makefile.am
++++ b/lib/Makefile.am
+@@ -30,6 +30,7 @@ AM_CPPFLAGS = -I$(top_srcdir)/gnu -I../ -I../gnu
+ AM_CFLAGS = $(GNULIB_WARN_CFLAGS) $(WERROR_CFLAGS)
+ 
+ noinst_HEADERS = \
++ mkdtempat.h\
+  paxlib.h\
+  rmt.h\
+  system.h\
+@@ -38,6 +39,7 @@ noinst_HEADERS = \
+  xattr-at.h
+ 
+ libtar_a_SOURCES = \
++  mkdtempat.c\
+   paxerror.c paxexit-status.c paxlib.h paxnames.c \
+   rtapelib.c \
+   rmt.h \
+diff --git a/lib/mkdtempat.c b/lib/mkdtempat.c
+new file mode 100644
+index 0000000..e35b9bb
+--- /dev/null
++++ b/lib/mkdtempat.c
+@@ -0,0 +1,45 @@
++/* Copyright 2026 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 <http://www.gnu.org/licenses/>.
++
++   Written by Paul Eggert.  */
++
++#include <config.h>
++
++#include "mkdtempat.h"
++
++#include <tempname.h>
++
++#include <stddef.h>
++#include <sys/stat.h>
++
++static int
++try_dir (char *tmpl, void *flags)
++{
++  int *pdirfd = flags;
++  return mkdirat (*pdirfd, tmpl, S_IRUSR | S_IWUSR | S_IXUSR);
++}
++
++/* Relative to the directory DIRFD if XTEMPLATE is relative,
++   generate a unique temporary directory from XTEMPLATE.
++   The last six characters of XTEMPLATE must be "XXXXXX";
++   replace them with a string that makes the generated directory unique.
++   Create the directory mode 700, and return its name.
++   On failure, return NULL and set errno.  */
++char *
++mkdtempat (int dirfd, char *xtemplate)
++{
++  return (try_tempname_len (xtemplate, 0, &dirfd, try_dir, 6) < 0
++	  ? NULL : xtemplate);
++}
+diff --git a/lib/mkdtempat.h b/lib/mkdtempat.h
+new file mode 100644
+index 0000000..03a18dc
+--- /dev/null
++++ b/lib/mkdtempat.h
+@@ -0,0 +1 @@
++char *mkdtempat (int, char *);
+diff --git a/src/incremen.c b/src/incremen.c
+index 194d5cb..a808e1c 100644
+--- a/src/incremen.c
++++ b/src/incremen.c
+@@ -19,6 +19,7 @@
+ 
+ #include <system.h>
+ #include <hash.h>
++#include <mkdtempat.h>
+ #include <quotearg.h>
+ #include "common.h"
+ 
+@@ -1653,13 +1654,16 @@ purge_directory (char const *directory_name)
+       if (*arc == 'X')
+ 	{
+ #define TEMP_DIR_TEMPLATE "tar.XXXXXX"
+-	  size_t len = strlen (arc + 1);
++	  char *d = safer_name_suffix (arc + 1, false, absolute_names_option);
++	  size_t len = strlen (d);
+ 	  temp_stub = xrealloc (temp_stub, len + 1 + sizeof TEMP_DIR_TEMPLATE);
+-	  memcpy (temp_stub, arc + 1, len);
+-	  temp_stub[len] = '/';
+-	  memcpy (temp_stub + len + 1, TEMP_DIR_TEMPLATE,
++	  char *copy_end = mempcpy (temp_stub, d, len);
++	  *copy_end = '/';
++	  memcpy (copy_end + !ISSLASH (copy_end[-1]), TEMP_DIR_TEMPLATE,
+ 		  sizeof TEMP_DIR_TEMPLATE);
+-	  if (!mkdtemp (temp_stub))
++	  struct fdbase f = fdbase (temp_stub);
++	  if (f.fd == BADFD
++	      || !mkdtempat (f.fd, temp_stub + (f.base - temp_stub)))
+ 	    {
+ 	      ERROR ((0, errno,
+ 		      _("Cannot create temporary directory using template %s"),
+diff --git a/tests/Makefile.am b/tests/Makefile.am
+index 3298ac6..d28bf25 100644
+--- a/tests/Makefile.am
++++ b/tests/Makefile.am
+@@ -208,6 +208,8 @@ TESTSUITE_AT = \
+  rename04.at\
+  rename05.at\
+  rename06.at\
++ rename08.at\
++ rename09.at\
+  remfiles01.at\
+  remfiles02.at\
+  remfiles03.at\
+diff --git a/tests/rename08.at b/tests/rename08.at
+new file mode 100644
+index 0000000..5452fb4
+--- /dev/null
++++ b/tests/rename08.at
+@@ -0,0 +1,131 @@
++# Process this file with autom4te to create testsuite. -*- Autotest -*-
++
++# Test suite for GNU tar.
++# Copyright 2006-2026 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/>.
++
++# Description: Handling of cyclic renames in incremental archives.
++
++AT_SETUP([cyclic renames])
++AT_KEYWORDS([incremental rename rename08 cyclic-rename])
++
++AT_TAR_CHECK([
++AT_SORT_PREREQ
++
++mkdir foo
++genfile --file foo/file1
++genfile --file foo/file2
++
++mkdir foo/a
++genfile --file foo/a/filea
++
++mkdir foo/b
++genfile --file foo/b/fileb
++
++mkdir foo/c
++genfile --file foo/c/filec
++
++sleep 1
++
++echo "First dump"
++echo "First dump">&2
++tar -g incr -cf arch.1 -v foo 2>tmperr
++sort tmperr >&2
++
++# Shuffle directories:
++(cd foo
++mv a $$
++mv c a
++mv b c
++mv $$ b)
++
++echo "Second dump"
++echo "Second dump" >&2
++tar -g incr -cf arch.2 -v foo 2>tmperr
++sort tmperr >&2
++
++mkdir -p restoreparent/restore
++cd restoreparent
++tar xfg ../arch.1 /dev/null --warning=no-timestamp -C restore
++
++echo "Begin directory listing 1"
++( cd restore; find foo ) | sort
++echo "End directory listing 1"
++
++tar xfgv ../arch.2 /dev/null --warning=no-timestamp -C restore
++echo Begin directory listing 2
++( cd restore ; find foo ) | sort
++echo End directory listing 2
++],
++[0],
++[First dump
++foo/
++foo/a/
++foo/b/
++foo/c/
++foo/file1
++foo/file2
++foo/a/filea
++foo/b/fileb
++foo/c/filec
++Second dump
++foo/
++foo/a/
++foo/b/
++foo/c/
++Begin directory listing 1
++foo
++foo/a
++foo/a/filea
++foo/b
++foo/b/fileb
++foo/c
++foo/c/filec
++foo/file1
++foo/file2
++End directory listing 1
++foo/
++foo/a/
++foo/b/
++foo/c/
++Begin directory listing 2
++foo
++foo/a
++foo/a/filec
++foo/b
++foo/b/filea
++foo/c
++foo/c/fileb
++foo/file1
++foo/file2
++End directory listing 2
++],
++[First dump
++tar: foo/a: Directory is new
++tar: foo/b: Directory is new
++tar: foo/c: Directory is new
++tar: foo: Directory is new
++Second dump
++tar: foo/a: Directory has been renamed from 'foo/c'
++tar: foo/b: Directory has been renamed from 'foo/a'
++tar: foo/c: Directory has been renamed from 'foo/b'
++],
++[],[],[gnu, oldgnu, posix])
++
++AT_CLEANUP
++
++# End of rename03.at
+diff --git a/tests/rename09.at b/tests/rename09.at
+new file mode 100644
+index 0000000..8346ff0
+--- /dev/null
++++ b/tests/rename09.at
+@@ -0,0 +1,129 @@
++# Process this file with autom4te to create testsuite. -*- Autotest -*-
++
++# Test suite for GNU tar.
++# Copyright 2006-2026 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/>.
++
++# Description: Handling of cyclic renames in incremental archives.
++
++AT_SETUP([cyclic renames])
++AT_KEYWORDS([incremental rename rename09 cyclic-rename])
++
++AT_TAR_CHECK([
++AT_SORT_PREREQ
++
++mkdir foo
++genfile --file foo/file1
++genfile --file foo/file2
++
++mkdir foo/a
++genfile --file foo/a/filea
++
++mkdir foo/b
++genfile --file foo/b/fileb
++
++mkdir foo/c
++genfile --file foo/c/filec
++
++sleep 1
++
++echo "First dump"
++echo "First dump">&2
++tar -g incr -cf arch.1 -v foo 2>tmperr
++sort tmperr >&2
++
++# Shuffle directories:
++(cd foo
++mv a $$
++mv c a
++mv b c
++mv $$ b)
++
++echo "Second dump"
++echo "Second dump" >&2
++tar -g incr -cf arch.2 -v foo 2>tmperr
++sort tmperr >&2
++
++tar xfg arch.1 /dev/null --warning=no-timestamp --one-top-level=restore
++
++echo "Begin directory listing 1"
++( cd restore; find foo ) | sort
++echo "End directory listing 1"
++
++tar xfgv arch.2 /dev/null --warning=no-timestamp --one-top-level=restore
++echo Begin directory listing 2
++( cd restore ; find foo ) | sort
++echo End directory listing 2
++],
++[0],
++[First dump
++foo/
++foo/a/
++foo/b/
++foo/c/
++foo/file1
++foo/file2
++foo/a/filea
++foo/b/fileb
++foo/c/filec
++Second dump
++foo/
++foo/a/
++foo/b/
++foo/c/
++Begin directory listing 1
++foo
++foo/a
++foo/a/filea
++foo/b
++foo/b/fileb
++foo/c
++foo/c/filec
++foo/file1
++foo/file2
++End directory listing 1
++foo/
++foo/a/
++foo/b/
++foo/c/
++Begin directory listing 2
++foo
++foo/a
++foo/a/filec
++foo/b
++foo/b/filea
++foo/c
++foo/c/fileb
++foo/file1
++foo/file2
++End directory listing 2
++],
++[First dump
++tar: foo/a: Directory is new
++tar: foo/b: Directory is new
++tar: foo/c: Directory is new
++tar: foo: Directory is new
++Second dump
++tar: foo/a: Directory has been renamed from 'foo/c'
++tar: foo/b: Directory has been renamed from 'foo/a'
++tar: foo/c: Directory has been renamed from 'foo/b'
++],
++[],[],[gnu, oldgnu, posix])
++
++AT_CLEANUP
++
++# End of rename03.at
+diff --git a/tests/testsuite.at b/tests/testsuite.at
+index 6baabf7..19cb484 100644
+--- a/tests/testsuite.at
++++ b/tests/testsuite.at
+@@ -400,6 +400,8 @@ m4_include([rename03.at])
+ m4_include([rename04.at])
+ m4_include([rename05.at])
+ m4_include([rename06.at])
++m4_include([rename08.at])
++m4_include([rename09.at])
+ m4_include([chtype.at])
+ 
+ AT_BANNER([Ignore failing reads])
diff --git a/tar-CVE-2026-18508.patch b/tar-CVE-2026-18508.patch
new file mode 100644
index 0000000..7ee5d90
--- /dev/null
+++ b/tar-CVE-2026-18508.patch
@@ -0,0 +1,775 @@
+# upstream bfc33463..941f62b2: confine --one-top-level hard link
+# targets to the top-level directory (CVE-2026-18508)
+diff --git a/doc/tar.texi b/doc/tar.texi
+index e924be6..cfac644 100644
+--- a/doc/tar.texi
++++ b/doc/tar.texi
+@@ -3266,14 +3266,14 @@ directory.
+ 
+ @opsummary{one-top-level}
+ @item --one-top-level[=@var{dir}]
+-Tells @command{tar} to create a new directory beneath the extraction directory
+-(or the one passed to @option{-C}) and use it to prevent @command{tar}
+-from modifying files outside that directory.
+-If @var{dir} is present, it must be a relative file name.
+-If it is absent, the name of the new directory
++Tells @command{tar} to use a directory beneath the extraction directory
++(or the one passed to @option{-C}) to prevent @command{tar}
++from accessing files outside that directory.
++If @var{dir} is absent, the name of the directory
+ is the base name of the archive minus any recognized archive suffix.
+-If multiple @option{-C} options are present,
+-each has its own subdirectory with the same name.
++If @var{dir} is an absolute file name, that is the only such directory;
++otherwise, the working directory and the directories specified by
++any @option{-C} options each have the named subdirectory.
+ Any member names that do not begin
+ with that directory name (after
+ transformations from @option{--transform} and
+@@ -3281,6 +3281,9 @@ transformations from @option{--transform} and
+ file name suffixes are @samp{.tar}, and any compression suffixes
+ recognizable by @xref{--auto-compress}.
+ 
++This option can be used only when extracting, comparing, or reading
++from the archive.
++
+ @opsummary{overwrite}
+ @item --overwrite
+ 
+diff --git a/src/common.h b/src/common.h
+index b4a5821..cc8dbe5 100644
+--- a/src/common.h
++++ b/src/common.h
+@@ -246,7 +246,6 @@ GLOBAL bool numeric_owner_option;
+ GLOBAL bool one_file_system_option;
+ 
+ /* Create a top-level directory for extracting based on the archive name.  */
+-GLOBAL bool one_top_level_option;
+ GLOBAL char *one_top_level_dir;
+ 
+ /* Specified value to be put into tar file in place of stat () results, or
+@@ -545,6 +544,7 @@ void verify_volume (void);
+ /* Module extract.c.  */
+ 
+ void extr_init (void);
++int make_directories (char *file_name, bool *interdir_made);
+ void extract_archive (void);
+ void extract_finish (void);
+ bool rename_directory (char *src, char *dst);
+@@ -635,6 +635,20 @@ void skim_member (bool must_copy);
+ #define min(a, b) ((a) < (b) ? (a) : (b))
+ #define max(a, b) ((a) < (b) ? (b) : (a))
+ 
++/* A directory FD, and a file name BASE that matches the regexp "[^/]*[/]*".
++   If BASE is absolute (i.e., matches "/+"), FD is valid but irrelevant.
++   Otherwise if FD == AT_FDCWD, BASE is relative to the current directory.
++   Otherwise if FD == BADFD, the parent directory could not be opened
++   and BASE is merely the original file name's basename.
++   Otherwise, FD is open to a parent directory,
++   and BASE is relative to that directory.
++   BASE points to storage managed elsewhere; do not free it directly.  */
++struct fdbase
++  {
++    int fd;
++    char const *base;
++  };
++
+ char const *quote_n_colon (int n, char const *arg);
+ void assign_string_or_null (char **dest, const char *src)
+   ATTRIBUTE_NONNULL ((1));
+@@ -657,6 +671,8 @@ void namebuf_free (namebuf_t buf);
+ char *namebuf_name (namebuf_t buf, const char *name);
+ 
+ const char *tar_dirname (void);
++char *transform_top_level (const char *name)
++  _GL_ATTRIBUTE_MALLOC _GL_ATTRIBUTE_DEALLOC_FREE;
+ 
+ /* Represent N using a signed integer I such that (uintmax_t) I == N.
+    With a good optimizing compiler, this is equivalent to (intmax_t) i
+@@ -728,10 +744,10 @@ size_t blocking_write (int fd, void const *buf, size_t count);
+ enum { BADFD = AT_FDCWD == -1 ? -2 : -1 };
+ 
+ extern int chdir_current;
+-int chdir_arg (char const *dir);
+-void chdir_do (int dir);
++int chdir_arg (char const *dir, bool one_top_level);
++void chdir_do (int dir, bool create);
+ 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 fdbase (char const *);
+ struct fdbase fdbase1 (char const *);
+ void fdbase_clear (void);
+ int chdir_count (void);
+diff --git a/src/create.c b/src/create.c
+index 845b194..08f77f1 100644
+--- a/src/create.c
++++ b/src/create.c
+@@ -1266,7 +1266,7 @@ ensure_slash (char **pstr)
+ /* If we just ran out of file descriptors, release a file descriptor
+    in the directory chain somewhere leading from DIR->parent->parent
+    up through the root.  Return true if successful, false (preserving
+-   errno == EMFILE) otherwise.
++   errno) otherwise.
+ 
+    Do not release DIR's file descriptor, or DIR's parent, as other
+    code assumes that they work.  On some operating systems, another
+@@ -1277,7 +1277,8 @@ ensure_slash (char **pstr)
+ static bool
+ open_failure_recover (struct tar_stat_info const *dir)
+ {
+-  if (errno == EMFILE && dir && dir->parent)
++  int err = errno;
++  if ((err == EMFILE || err == ENFILE) && dir && dir->parent)
+     {
+       struct tar_stat_info *p;
+       for (p = dir->parent->parent; p; p = p->parent)
+@@ -1286,7 +1287,7 @@ open_failure_recover (struct tar_stat_info const *dir)
+ 	    tar_stat_close (p);
+ 	    return true;
+ 	  }
+-      errno = EMFILE;
++      errno = err;
+     }
+ 
+   return false;
+diff --git a/src/extract.c b/src/extract.c
+index d1c86a0..49e4dd4 100644
+--- a/src/extract.c
++++ b/src/extract.c
+@@ -698,7 +698,7 @@ fixup_delayed_set_stat (char const *src, char const *dst)
+    create all required directories.  Return zero if all the required
+    directories were created, nonzero (issuing a diagnostic) otherwise.
+    Set *INTERDIR_MADE (unless NULL) if at least one directory was created. */
+-static int
++int
+ make_directories (char *file_name, bool *interdir_made)
+ {
+   char *cursor0 = file_name + FILE_SYSTEM_PREFIX_LEN (file_name);
+@@ -741,7 +741,7 @@ make_directories (char *file_name, bool *interdir_made)
+ 	     repair_delayed_set_stat may need to update the struct.  */
+ 	  delay_set_stat (file_name,
+ 			  0, mode & ~ current_umask, MODE_RWX,
+-			  desired_mode, AT_SYMLINK_NOFOLLOW);
++			    desired_mode, AT_SYMLINK_NOFOLLOW);
+ 	  if (interdir_made)
+ 	    *interdir_made = true;
+ 	  print_for_mkdir (file_name, desired_mode);
+@@ -974,7 +974,7 @@ apply_nonancestor_delayed_set_stat (char const *file_name, bool after_links)
+ 	      && memcmp (file_name, data->file_name, data->file_name_len) == 0))
+ 	break;
+ 
+-      chdir_do (data->change_dir);
++      chdir_do (data->change_dir, false);
+ 
+       if (check_for_renamed_directories)
+ 	{
+@@ -1869,7 +1869,7 @@ extract_archive (void)
+     {
+       int dir = chdir_current;
+       apply_nonancestor_delayed_set_stat (current_stat_info.file_name, 0);
+-      chdir_do (dir);
++      chdir_do (dir, false);
+     }
+ 
+   /* Take a safety backup of a previously existing file.  */
+@@ -1889,8 +1889,35 @@ extract_archive (void)
+   typeflag = sparse_member_p (&current_stat_info) ?
+                   GNUTYPE_SPARSE : current_header->header.typeflag;
+ 
+-  bool ok = prepare_to_extract (current_stat_info.file_name, typeflag, &fun)
+-            && fun (current_stat_info.file_name, typeflag) == 0;
++  bool ok = false;
++  if (prepare_to_extract (current_stat_info.file_name, typeflag, &fun))
++    {
++      if (one_top_level_dir)
++	{
++	  /* Create one_top_level dir if it does not exist.  */
++	  chdir_do (chdir_current, true);
++	  /* Flush delayed stat to mirror the code above that does it
++	     before extracting a new entry. Creating the one_top_level
++	     dir may have created new delayed_set_stat interdir
++	     entries, so repeat the operation. Ideally this should not
++	     be needed, but the newly-created interdir entries have
++	     st_dev/st_ino uninitialized, which would be a problem if
++	     there is a "." entry afterwards:
++	     apply_nonancestor_delayed_set_stat would use the
++	     uninitialized values. Ideally, st_dev/st_ino would be
++	     initialized by mark_metadata_set, but this one does not
++	     take chdir into account, so it stats a wrong file. */
++	  if (!delay_directory_restore_option)
++	    {
++	      int dir = chdir_current;
++	      apply_nonancestor_delayed_set_stat (current_stat_info.file_name,
++						  false);
++	      chdir_do (dir, false);
++	    }
++	}
++      if (fun (current_stat_info.file_name, typeflag) == 0)
++	ok = true;
++    }
+   skip_member ();
+   if (!ok && backup_option)
+     undo_last_backup ();
+@@ -1903,7 +1930,7 @@ apply_delayed_link (struct delayed_link *ds)
+   struct string_list *sources = ds->sources;
+   char const *valid_source = NULL;
+ 
+-  chdir_do (ds->change_dir);
++  chdir_do (ds->change_dir, false);
+ 
+   for (sources = ds->sources; sources; sources = sources->next)
+     {
+diff --git a/src/list.c b/src/list.c
+index b560569..dab6c3c 100644
+--- a/src/list.c
++++ b/src/list.c
+@@ -128,16 +128,28 @@ enforce_one_top_level (char **pfile_name)
+       int pos = strlen (one_top_level_dir);
+       if (strncmp (p, one_top_level_dir, pos) == 0)
+ 	{
+-	  if (ISSLASH (p[pos]) || p[pos] == 0)
+-	    return;
++	  /* Remove the one_top_level_dir prefix if it ends at
++	     component boundary.  */
++	  if (ISSLASH (p[pos]))
++	    {
++	      *pfile_name = xstrdup (p[pos+1] ? &p[pos+1] : ".");
++	      free (file_name);
++	      return;
++	    }
++	  else if (p[pos] == 0)
++	    {
++	      *pfile_name = xstrdup (".");
++	      free (file_name);
++	      return;
++	    }
+ 	}
+-
+-      *pfile_name = make_file_name (one_top_level_dir, file_name);
+-      normalize_filename_x (*pfile_name);
++      /* If the prefix does not match, do nothing.  */
+     }
+   else
+-    *pfile_name = xstrdup (one_top_level_dir);
+-  free (file_name);
++    {
++      *pfile_name = xstrdup (".");
++      free (file_name);
++    }
+ }
+ 
+ void
+@@ -158,8 +170,15 @@ transform_stat_info (int typeflag, struct tar_stat_info *stat_info)
+       transform_member_name (&stat_info->link_name, XFORM_LINK);
+     }
+ 
+-  if (one_top_level_option)
+-    enforce_one_top_level (&current_stat_info.file_name);
++  if (one_top_level_dir)
++    {
++      enforce_one_top_level (&stat_info->file_name);
++      /* Hard links are interpreted relative to cwd, and --one-top-level
++	 works by means of a hidden change of cwd to the requested directory.
++	 Adjust hard link targets as well.  */
++      if (typeflag == LNKTYPE)
++	enforce_one_top_level (&stat_info->link_name);
++    }
+ }
+ 
+ /* Main loop for reading an archive.  */
+@@ -1168,9 +1187,9 @@ simple_print_header (struct tar_stat_info *st, union block *blk,
+   int sizelen;
+ 
+   if (show_transformed_names_option)
+-    temp_name = st->file_name ? st->file_name : st->orig_file_name;
++    temp_name = transform_top_level (st->file_name ? st->file_name : st->orig_file_name);
+   else
+-    temp_name = st->orig_file_name ? st->orig_file_name : st->file_name;
++    temp_name = xstrdup (st->orig_file_name ? st->orig_file_name : st->file_name);
+ 
+   if (block_number_option)
+     {
+@@ -1363,6 +1382,7 @@ simple_print_header (struct tar_stat_info *st, union block *blk,
+     }
+   fflush (stdlis);
+   xattrs_print (st);
++  free (temp_name);
+ }
+ 
+ 
+diff --git a/src/misc.c b/src/misc.c
+index 0e874eb..e14df2b 100644
+--- a/src/misc.c
++++ b/src/misc.c
+@@ -23,6 +23,7 @@
+ #include <xgetcwd.h>
+ #include <unlinkdir.h>
+ #include <utimens.h>
++#include <assert.h>
+ 
+ #ifndef DOUBLE_SLASH_IS_DISTINCT_ROOT
+ # define DOUBLE_SLASH_IS_DISTINCT_ROOT 0
+@@ -946,6 +947,7 @@ struct wd
+      to be used.  */
+   int fd;
+ 
++  bool one_top_level;
+   /* 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.  */
+@@ -977,9 +979,17 @@ static size_t wdcache_count;
+ int
+ chdir_count (void)
+ {
+-  if (wd_count == 0)
+-    return wd_count;
+-  return wd_count - 1;
++  int count = 0;
++  if (wd_count)
++    {
++      /* Do not count the initial CWD entry -> start at 1.  */
++      for (int i = 1; i < wd_count; i++)
++	{
++	  if (! wd[i].one_top_level)
++	    count++;
++	}
++    }
++  return count;
+ }
+ 
+ /* Grow the WD table by at least one entry.  */
+@@ -996,15 +1006,27 @@ grow_wd (void)
+       wd[wd_count].abspath = NULL;
+       wd[wd_count].fd = AT_FDCWD;
+       wd[wd_count].id.err = -1;
++      wd[wd_count].one_top_level = false;
+       wd_count++;
++      if (one_top_level_dir)
++	{
++	  wd[wd_count].name = one_top_level_dir;
++	  wd[wd_count].abspath = NULL;
++	  wd[wd_count].fd = 0;
++	  wd[wd_count].id.err = -1;
++	  wd[wd_count].one_top_level = true;
++	  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)
++chdir_arg (char const *dir, bool one_top_level)
+ {
++  if (one_top_level)
++    chdir_arg (dir, false);
+   if (wd_count == wd_alloc)
+     grow_wd ();
+ 
+@@ -1014,13 +1036,22 @@ chdir_arg (char const *dir)
+     {
+       dir += dotslashlen (dir);
+       if (! dir[dir[0] == '.'])
+-	return wd_count - 1;
++	{
++	  if (wd[wd_count - 1].one_top_level == one_top_level)
++	    return wd_count - 1;
++	  else
++	    return wd_count - 2;
++	}
+     }
+ 
++  if (one_top_level)
++    dir = one_top_level_dir;
++
+   wd[wd_count].name = dir;
+   wd[wd_count].abspath = NULL;
+   wd[wd_count].fd = 0;
+   wd[wd_count].id.err = -1;
++  wd[wd_count].one_top_level = one_top_level;
+   return wd_count++;
+ }
+ 
+@@ -1039,21 +1070,74 @@ static int chdir_fd = AT_FDCWD;
+    working directory; otherwise, I must be a value returned by
+    chdir_arg.  */
+ void
+-chdir_do (int i)
++chdir_do (int i, bool create)
+ {
+-  if (chdir_current != i)
+-    {
+-      struct wd *curr = &wd[i];
+-      int fd = curr->fd;
++  struct wd *curr = &wd[i];
++  int fd = curr->fd;
+ 
+-      if (! fd)
++  /* Nothing to create unless we are at the one_top_level dir that has
++     not been created yet.  */
++  create = create && curr->one_top_level && (fd == BADFD || fd == 0);
++
++  if (chdir_current != i || create)
++    {
++      if (! fd || create)
+ 	{
+ 	  if (! IS_ABSOLUTE_FILE_NAME (curr->name))
+-	    chdir_do (i - 1);
++	    {
++	      int j = i - 1;
++	      if (wd[j].one_top_level)
++		{
++		  j--;
++		  assert (! wd[j].one_top_level);
++		}
++	      chdir_do (j, false);
++	    }
+ 	  fd = openat (chdir_fd, curr->name,
+ 		       open_searchdir_how.flags & ~O_NOFOLLOW);
+ 	  if (fd < 0)
+-	    open_fatal (curr->name);
++	    {
++	      if (create)
++		{
++		  char *dir_with_dot;
++		  struct open_how saved_open_searchdir_how = open_searchdir_how;
++		  /* Don't use O_BENEATH during creation of the
++		     directory. The one-top-level directory is
++		     allowed to be given as an absolute path.  */
++		  open_searchdir_how.resolve = 0;
++		  /* Append a dot. make_directories creates
++		     directories up to and excluding the last
++		     component of the path. So, in order to create
++		     "a/b", we need to pass "a/b/." to it. */
++		  {
++		    namebuf_t nbuf = namebuf_create (curr->name);
++		    namebuf_add_dir (nbuf, ".");
++		    dir_with_dot = namebuf_finish (nbuf);
++		  }
++		  if (make_directories (dir_with_dot, NULL) == 0)
++		    /* Directory created, retry */
++		    fd = openat (chdir_fd, curr->name,
++				 open_searchdir_how.flags & ~O_NOFOLLOW);
++		  open_searchdir_how = saved_open_searchdir_how;
++		  free (dir_with_dot);
++		  /* Either the creation or open failed */
++		  if (fd < 0)
++		    open_fatal (curr->name);
++		}
++	      else if (errno == ENOENT && curr->one_top_level)
++		{
++		  /* We are requested to not create the directory now. Mark it
++		     as to be created later when called with create == true. */
++		  chdir_fd = curr->fd = BADFD;
++		  chdir_current = i;
++		  /* Do not add it to the cache */
++		  return;
++		}
++	      else
++		{
++		  open_fatal (curr->name);
++		}
++	    }
+ 
+ 	  curr->fd = fd;
+ 
+@@ -1071,7 +1155,7 @@ chdir_do (int i)
+ 	    }
+ 	}
+ 
+-      if (0 < fd)
++      if (0 < fd && /* no assumption about sign of BADFD */ fd != BADFD)
+ 	{
+ 	  /* Move the i value to the front of the cache.  This is
+ 	     O(CHDIR_CACHE_SIZE), but the cache is small.  */
+@@ -1175,6 +1259,14 @@ fdbase_opendir (char const *file_name, bool alternate)
+ {
+   char const *name = file_name;
+ 
++  if (chdir_fd == BADFD && ! IS_ABSOLUTE_FILE_NAME (file_name))
++    {
++      /* BADFD is a sentinel value meaning that the chdir directory
++	 needs to be created lazily, therefore if we encounter it, the
++	 directory does not exist yet. */
++      errno = ENOENT;
++      return (struct fdbase) { .fd = chdir_fd, .base = name };
++    }
+   /* Skip past leading "./"s,
+      but not past the last "./" if that ends the name.  */
+   idx_t dslen = dotslashlen (name);
+@@ -1219,13 +1311,13 @@ fdbase_opendir (char const *file_name, bool alternate)
+ 	  if (subfd < 0)
+ 	    {
+ 	      /* Keep the old directory cached and report open failure,
+-		 unless EMFILE means it's possible that falling
++		 unless EMFILE/ENFILE 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)
++	      if (errno != EMFILE && errno != ENFILE)
+ 		return (struct fdbase) { .fd = BADFD, .base = base };
+ 	    }
+ 	  else
+@@ -1279,6 +1371,29 @@ tar_dirname (void)
+   return wd[chdir_current].name;
+ }
+ 
++/* Return a newly allocated string that shows NAME from the user's
++   viewpoint, given that --one-top-level may be in effect.  */
++char *
++transform_top_level (const char *name)
++{
++  if (wd[chdir_current].one_top_level)
++    {
++      if (strcmp (name, ".") == 0)
++	{
++	  /* nothing to append - .../. is the same as ... */
++	  return xstrdup (wd[chdir_current].name);
++	}
++      else
++	{
++	  namebuf_t nbuf = namebuf_create (wd[chdir_current].name);
++	  namebuf_add_dir (nbuf, name);
++	  return namebuf_finish (nbuf);
++	}
++    }
++  else
++    return xstrdup (name);
++}
++
+ /* Return the absolute path that represents the working
+    directory referenced by IDX.
+ 
+@@ -1307,12 +1422,13 @@ tar_getcdpath (int idx)
+       int save_cwdi = chdir_current;
+ 
+       for (i = idx; i >= 0; i--)
+-	if (wd[i].abspath)
++	if (wd[i].abspath && !wd[i].one_top_level)
+ 	  break;
+ 
+       while (++i <= idx)
+ 	{
+-	  chdir_do (i);
++	  if (!wd[i].one_top_level)
++	    chdir_do (i, false);
+ 	  if (i == 0)
+ 	    {
+ 	      if ((wd[i].abspath = xgetcwd ()) == NULL)
+@@ -1325,13 +1441,18 @@ tar_getcdpath (int idx)
+ 	    wd[i].abspath = xstrdup (wd[i].name);
+ 	  else
+ 	    {
+-	      namebuf_t nbuf = namebuf_create (wd[i - 1].abspath);
++	      int j = i - 1;
++	      if (wd[j].one_top_level)
++		{
++		  j--;
++		  assert (! wd[j].one_top_level);
++		}
++	      namebuf_t nbuf = namebuf_create (wd[j].abspath);
+ 	      namebuf_add_dir (nbuf, wd[i].name);
+ 	      wd[i].abspath = namebuf_finish (nbuf);
+ 	    }
+ 	}
+-
+-      chdir_do (save_cwdi);
++      chdir_do (save_cwdi, false);
+     }
+ 
+   return wd[idx].abspath;
+diff --git a/src/names.c b/src/names.c
+index 54fd696..eb063fe 100644
+--- a/src/names.c
++++ b/src/names.c
+@@ -868,6 +868,7 @@ name_init (void)
+ {
+   name_buffer = xmalloc (NAME_FIELD_SIZE + 2);
+   name_buffer_length = NAME_FIELD_SIZE;
++  chdir_do (chdir_arg (".", !!one_top_level_dir), false);
+   name_list_adjust ();
+ }
+ 
+@@ -1120,7 +1121,8 @@ name_next_elt (int change_dirs)
+ 	case NELT_CHDIR:
+ 	  if (change_dirs)
+ 	    {
+-	      chdir_do (chdir_arg (xstrdup (ep->v.name)));
++	      chdir_do (chdir_arg (xstrdup (ep->v.name), !!one_top_level_dir),
++			false);
+ 	      name_list_advance ();
+ 	      break;
+ 	    }
+@@ -1183,7 +1185,7 @@ name_gather (void)
+       static int change_dir;
+ 
+       while ((ep = name_next_elt (0)) && ep->type == NELT_CHDIR)
+-	change_dir = chdir_arg (xstrdup (ep->v.name));
++	change_dir = chdir_arg (xstrdup (ep->v.name), !!one_top_level_dir);
+ 
+       if (ep)
+ 	{
+@@ -1212,7 +1214,7 @@ name_gather (void)
+ 	{
+ 	  int change_dir0 = change_dir;
+ 	  while ((ep = name_next_elt (0)) && ep->type == NELT_CHDIR)
+-	    change_dir = chdir_arg (xstrdup (ep->v.name));
++	    change_dir = chdir_arg (xstrdup (ep->v.name), !!one_top_level_dir);
+ 
+ 	  if (ep)
+ 	    addname (ep->v.name, change_dir, true, NULL);
+@@ -1327,7 +1329,7 @@ name_match (const char *file_name)
+ 
+       if (cursor->name[0] == 0)
+ 	{
+-	  chdir_do (cursor->change_dir);
++	  chdir_do (cursor->change_dir, false);
+ 	  namelist = NULL;
+ 	  nametail = NULL;
+ 	  return true;
+@@ -1349,7 +1351,7 @@ name_match (const char *file_name)
+ 	  if (!(ISSLASH (file_name[cursor->length]) && recursion_option)
+ 	      || cursor->found_count == 0)
+ 	    cursor->found_count++; /* remember it matched */
+-	  chdir_do (cursor->change_dir);
++	  chdir_do (cursor->change_dir, false);
+ 	  /* We got a match.  */
+ 	  return ISFOUND (cursor);
+ 	}
+@@ -1783,7 +1785,7 @@ collect_and_sort_names (void)
+ 	/* NOTE: EXCLUDE_ANCHORED is not relevant here */
+ 	/* FIXME: just skip regexps for now */
+ 	continue;
+-      chdir_do (name->change_dir);
++      chdir_do (name->change_dir, false);
+ 
+       if (name->name[0] == 0)
+ 	continue;
+@@ -1929,7 +1931,7 @@ name_from_list (void)
+     {
+       if (!gnu_list_name->is_wildcard)
+ 	gnu_list_name->found_count++;
+-      chdir_do (gnu_list_name->change_dir);
++      chdir_do (gnu_list_name->change_dir, false);
+       return gnu_list_name;
+     }
+   return NULL;
+diff --git a/src/tar.c b/src/tar.c
+index 9a5fbd0..56c9ed2 100644
+--- a/src/tar.c
++++ b/src/tar.c
+@@ -1572,7 +1572,6 @@ parse_opt (int key, char *arg, struct argp_state *state)
+ 
+     case ONE_TOP_LEVEL_OPTION:
+       optloc_save (OC_ONE_TOP_LEVEL, args->loc);
+-      one_top_level_option = true;
+       one_top_level_dir = arg;
+       break;
+ 
+@@ -2576,9 +2575,11 @@ decode_options (int argc, char **argv)
+ 	same_order_option = false;
+     }
+ 
+-  if (one_top_level_option)
++  if (optloc_lookup (OC_ONE_TOP_LEVEL))
+     {
+-      char *base;
++      if (!IS_SUBCOMMAND_CLASS (SUBCL_READ))
++	option_conflict_error ("--one-top-level",
++			       subcommand_string (subcommand_option));
+ 
+       if (absolute_names_option)
+ 	{
+@@ -2589,17 +2590,18 @@ decode_options (int argc, char **argv)
+ 
+ 	  if (optloc_eq (one_top_level_loc, absolute_names_loc))
+ 	    option_conflict_error ("--one-top-level", "--absolute-names");
+-	  else if (one_top_level_loc->source == OPTS_COMMAND_LINE)
++	  if (one_top_level_loc->source == OPTS_COMMAND_LINE)
+ 	    absolute_names_option = false;
+ 	  else
+-	    one_top_level_option = false;
++	    one_top_level_dir = NULL;
+ 	}
+ 
+-      if (!one_top_level_dir && one_top_level_option)
++      if (!absolute_names_option && !one_top_level_dir)
+ 	{
+-	  /* If the user wants to guarantee that everything is under one
+-	     directory, determine its name now and let it be created later.  */
+-	  base = base_name (archive_name_array[0]);
++	  /* Determine name now; the directory (or directories, if -C
++	     means there are multiple top-level directories) are
++	     created later if needed.  */
++	  char *base = base_name (archive_name_array[0]);
+ 	  one_top_level_dir = strip_compression_suffix (base);
+ 	  free (base);
+ 
+@@ -2609,9 +2611,7 @@ decode_options (int argc, char **argv)
+ 			    "please set it explicitly with --one-top-level=DIR")));
+ 	}
+ 
+-      if (one_top_level_dir && !IS_RELATIVE_FILE_NAME (one_top_level_dir))
+-	USAGE_ERROR ((0, 0,
+-		     _("--one-top-level=DIR must use a relative file name")));
++      normalize_filename_x (one_top_level_dir);
+     }
+ 
+   /* If ready to unlink hierarchies, so we are for simpler files.  */
+diff --git a/src/unlink.c b/src/unlink.c
+index 33d2ccd..e199bab 100644
+--- a/src/unlink.c
++++ b/src/unlink.c
+@@ -103,7 +103,7 @@ flush_deferred_unlinks (bool force)
+       if (force
+ 	  || records_written > p->records_written + deferred_unlink_delay)
+ 	{
+-	  chdir_do (p->dir_idx);
++	  chdir_do (p->dir_idx, false);
+ 	  if (p->is_dir)
+ 	    {
+ 	      const char *fname;
+@@ -175,11 +175,11 @@ flush_deferred_unlinks (bool force)
+ 	  struct deferred_unlink *next = p->next;
+ 	  const char *fname;
+ 
+-	  chdir_do (p->dir_idx);
++	  chdir_do (p->dir_idx, false);
+ 	  if (p->dir_idx && IS_CWD (p))
+ 	    {
+ 	      fname = tar_dirname ();
+-	      chdir_do (p->dir_idx - 1);
++	      chdir_do (p->dir_idx - 1, false);
+ 	    }
+ 	  else
+ 	    fname = p->file_name;
+@@ -195,9 +195,9 @@ flush_deferred_unlinks (bool force)
+ 	  p = next;
+ 	}
+       dunlink_head = dunlink_tail = NULL;
+-    }	  
+-	    
+-  chdir_do (saved_chdir);
++    }
++
++  chdir_do (saved_chdir, false);
+ }
+ 
+ void
+diff --git a/src/update.c b/src/update.c
+index 2bc3820..2416777 100644
+--- a/src/update.c
++++ b/src/update.c
+@@ -141,7 +141,7 @@ update_archive (void)
+ 	      {
+ 		struct stat s;
+ 
+-		chdir_do (name->change_dir);
++		chdir_do (name->change_dir, false);
+ 		if (deref_stat (current_stat_info.file_name, &s) == 0)
+ 		  {
+ 		    if (S_ISDIR (s.st_mode))
diff --git a/tar-CVE-2026-5704.patch b/tar-CVE-2026-5704.patch
new file mode 100644
index 0000000..93287a6
--- /dev/null
+++ b/tar-CVE-2026-5704.patch
@@ -0,0 +1,472 @@
+# upstream b8d8a61b: make -t and -x agree on nonzero size fields of
+# symlinks, devices and fifos (CVE-2026-5704)
+diff --git a/src/extract.c b/src/extract.c
+index ae03190..d1c86a0 100644
+--- a/src/extract.c
++++ b/src/extract.c
+@@ -1056,7 +1056,7 @@ safe_dir_mode (struct stat const *st)
+ /* Extractor functions for various member types */
+ 
+ static int
+-extract_dir (char *file_name, int typeflag)
++extract_dir (char *file_name, MAYBE_UNUSED int typeflag)
+ {
+   int status;
+   mode_t mode;
+@@ -1081,8 +1081,6 @@ extract_dir (char *file_name, int typeflag)
+   if (incremental_option)
+     /* Read the entry and delete files that aren't listed in the archive.  */
+     purge_directory (file_name);
+-  else if (typeflag == GNUTYPE_DUMPDIR)
+-    skip_member ();
+ 
+   mode = safe_dir_mode (&current_stat_info.stat);
+ 
+@@ -1291,10 +1289,7 @@ extract_file (char *file_name, int typeflag)
+     {
+       fd = sys_exec_command (file_name, 'f', &current_stat_info);
+       if (fd < 0)
+-	{
+-	  skip_member ();
+-	  return 0;
+-	}
++	return 0;
+     }
+   else
+     {
+@@ -1314,7 +1309,6 @@ extract_file (char *file_name, int typeflag)
+ 	  int recover = maybe_recoverable (file_name, true, &interdir_made);
+ 	  if (recover != RECOVER_OK)
+ 	    {
+-	      skip_member ();
+ 	      if (recover == RECOVER_SKIP)
+ 		return 0;
+ 	      open_error (file_name);
+@@ -1362,6 +1356,7 @@ extract_file (char *file_name, int typeflag)
+       }
+ 
+   skim_file (size, false);
++  current_stat_info.skipped = true;
+ 
+   mv_end ();
+ 
+@@ -1894,15 +1889,10 @@ extract_archive (void)
+   typeflag = sparse_member_p (&current_stat_info) ?
+                   GNUTYPE_SPARSE : current_header->header.typeflag;
+ 
+-  if (prepare_to_extract (current_stat_info.file_name, typeflag, &fun))
+-    {
+-      if (fun (current_stat_info.file_name, typeflag) == 0)
+-	return;
+-    }
+-  else
+-    skip_member ();
+-
+-  if (backup_option)
++  bool ok = prepare_to_extract (current_stat_info.file_name, typeflag, &fun)
++            && fun (current_stat_info.file_name, typeflag) == 0;
++  skip_member ();
++  if (!ok && backup_option)
+     undo_last_backup ();
+ }
+ 
+diff --git a/src/incremen.c b/src/incremen.c
+index 7bcfdb9..194d5cb 100644
+--- a/src/incremen.c
++++ b/src/incremen.c
+@@ -1625,8 +1625,8 @@ dumpdir_ok (char *dumpdir)
+ 
+ /* Examine the directories under directory_name and delete any
+    files that were not there at the time of the back-up. */
+-static bool
+-try_purge_directory (char const *directory_name)
++void
++purge_directory (char const *directory_name)
+ {
+   char *current_dir;
+   char *cur, *arc, *p;
+@@ -1634,18 +1634,18 @@ try_purge_directory (char const *directory_name)
+   struct dumpdir *dump;
+ 
+   if (!is_dumpdir (&current_stat_info))
+-    return false;
++    return;
+ 
+   current_dir = tar_savedir (directory_name, 0);
+ 
+   if (!current_dir)
+     /* The directory doesn't exist now.  It'll be created.  In any
+        case, we don't have to delete any files out of it.  */
+-    return false;
++    return;
+ 
+   /* Verify if dump directory is sane */
+   if (!dumpdir_ok (current_stat_info.dumpdir))
+-    return false;
++    return;
+ 
+   /* Process renames */
+   for (arc = current_stat_info.dumpdir; *arc; arc += strlen (arc) + 1)
+@@ -1666,7 +1666,7 @@ try_purge_directory (char const *directory_name)
+ 		      quote (temp_stub)));
+ 	      free (temp_stub);
+ 	      free (current_dir);
+-	      return false;
++	      return;
+ 	    }
+ 	}
+       else if (*arc == 'R')
+@@ -1700,7 +1700,7 @@ try_purge_directory (char const *directory_name)
+ 	      free (current_dir);
+ 	      /* FIXME: Make sure purge_directory(dst) will return
+ 		 immediately */
+-	      return false;
++	      return;
+ 	    }
+ 	}
+     }
+@@ -1758,14 +1758,6 @@ try_purge_directory (char const *directory_name)
+   dumpdir_free (dump);
+ 
+   free (current_dir);
+-  return true;
+-}
+-
+-void
+-purge_directory (char const *directory_name)
+-{
+-  if (!try_purge_directory (directory_name))
+-    skip_member ();
+ }
+ 
+ void
+diff --git a/src/list.c b/src/list.c
+index f963d73..b560569 100644
+--- a/src/list.c
++++ b/src/list.c
+@@ -446,20 +446,15 @@ read_header (union block **return_block, struct tar_stat_info *info,
+       if ((status = tar_checksum (header, false)) != HEADER_SUCCESS)
+ 	break;
+ 
+-      /* Good block.  Decode file size and return.  */
+-
+-      if (header->header.typeflag == LNKTYPE)
+-	info->stat.st_size = 0;	/* links 0 size on tape */
+-      else
++      info->stat.st_size = OFF_FROM_HEADER (header->header.size);
++      if (info->stat.st_size < 0)
+ 	{
+-	  info->stat.st_size = OFF_FROM_HEADER (header->header.size);
+-	  if (info->stat.st_size < 0)
+-	    {
+-	      status = HEADER_FAILURE;
+-	      break;
+-	    }
++	  status = HEADER_FAILURE;
++	  break;
+ 	}
+ 
++      info->skipped = false;
++
+       if (header->header.typeflag == GNUTYPE_LONGNAME
+ 	  || header->header.typeflag == GNUTYPE_LONGLINK
+ 	  || header->header.typeflag == XHDTYPE
+@@ -522,11 +517,15 @@ read_header (union block **return_block, struct tar_stat_info *info,
+ 		}
+ 
+ 	      *bp = '\0';
++	      info->skipped = true;
+ 	    }
+ 	  else if (header->header.typeflag == XHDTYPE
+ 		   || header->header.typeflag == SOLARIS_XHDTYPE)
+-	    xheader_read (&info->xhdr, header,
+-			  OFF_FROM_HEADER (header->header.size));
++	    {
++	      xheader_read (&info->xhdr, header,
++			    OFF_FROM_HEADER (header->header.size));
++	      info->skipped = true;
++	    }
+ 	  else if (header->header.typeflag == XGLTYPE)
+ 	    {
+ 	      struct xheader xhdr;
+@@ -540,6 +539,7 @@ read_header (union block **return_block, struct tar_stat_info *info,
+ 			    OFF_FROM_HEADER (header->header.size));
+ 	      xheader_decode_global (&xhdr);
+ 	      xheader_destroy (&xhdr);
++	      info->skipped = true;
+ 	      if (mode == read_header_x_global)
+ 		{
+ 		  status = HEADER_SUCCESS_EXTENDED;
+@@ -556,6 +556,22 @@ read_header (union block **return_block, struct tar_stat_info *info,
+ 	  struct posix_header const *h = &header->header;
+ 	  char namebuf[sizeof h->prefix + 1 + NAME_FIELD_SIZE + 1];
+ 
++	  switch (h->typeflag)
++	    {
++	    /* For these file types, although POSIX does not specify the
++	       meaning of the size, it does say there should be no data,
++	       so treat the size as zero.  */
++	    case BLKTYPE: case CHRTYPE: case FIFOTYPE:
++
++	    /* For these file types, POSIX requires that the size be zero.
++	       Be generous and accept any size as zero, as some
++	       nonconforming programs generate nonzero size fields along
++	       with no data.  */
++	    case LNKTYPE: case SYMTYPE:
++
++	      info->stat.st_size = 0;
++	    }
++
+ 	  free (recent_long_name);
+ 
+ 	  if (next_long_name)
+@@ -1449,6 +1465,24 @@ skip_member (void)
+   skim_member (false);
+ }
+ 
++static bool
++member_is_dir (struct tar_stat_info *info, char typeflag)
++{
++  switch (typeflag)
++    {
++    case AREGTYPE:
++    case REGTYPE:
++    case CONTTYPE:
++      return info->had_trailing_slash;
++
++    case DIRTYPE:
++      return true;
++
++    default:
++      return false;
++    }
++}
<Skipped 878 lines>
================================================================

---- gitweb:

http://git.pld-linux.org/gitweb.cgi/packages/tar.git/commitdiff/adaef9d884f75901a38e4d96e12c2f47b9b16714



More information about the pld-cvs-commit mailing list